fzf-vcal/src/main.sh

460 lines
11 KiB
Bash
Executable File

#!/bin/sh
set -eu
# TODO: Make sensitive to failures. I don't want to miss appointments!
# TODO Ensure safe use of delimiters
err() {
echo "$1" >/dev/tty
}
if [ -z "${FZF_VCAL_USE_EXPORTED:-}" ]; then
# Read configuration
CONFIGFILE="$HOME/.config/fzf-vcal/config"
if [ ! -f "$CONFIGFILE" ]; then
err "Configuration '$CONFIGFILE' not found."
exit 1
fi
# shellcheck source=/dev/null
. "$CONFIGFILE"
if [ -z "${ROOT:-}" ] || [ -z "${SYNC_CMD:-}" ] || [ -z "${COLLECTION_LABELS:-}" ]; then
err "Configuration is incomplete."
exit 1
fi
export ROOT
export SYNC_CMD
export COLLECTION_LABELS
DAY_START=${DAY_START:-8}
DAY_END=${DAY_END:-18}
export DAY_START
export DAY_END
# Tools
if command -v "fzf" >/dev/null; then
FZF="fzf"
else
err "Did not find the command-line fuzzy finder fzf."
exit 1
fi
export FZF
if command -v "uuidgen" >/dev/null; then
UUIDGEN="uuidgen"
else
err "Did not find the uuidgen command."
exit 1
fi
export UUIDGEN
if command -v "bat" >/dev/null; then
CAT="bat"
elif command -v "batcat" >/dev/null; then
CAT="batcat"
fi
CAT=${CAT:+$CAT --color=always --style=numbers --language=md}
CAT=${CAT:-cat}
export CAT
### AWK SCRIPTS
AWK_LINES=$(
cat <<'EOF'
@@include src/awk/lines.awk
EOF
)
export AWK_LINES
AWK_MERGE=$(
cat <<'EOF'
@@include src/awk/merge.awk
EOF
)
export AWK_MERGE
AWK_PARSE=$(
cat <<'EOF'
@@include src/awk/parse.awk
EOF
)
export AWK_PARSE
AWK_WEEKVIEW=$(
cat <<'EOF'
@@include src/awk/weekview.awk
EOF
)
export AWK_WEEKVIEW
AWK_DAYVIEW=$(
cat <<'EOF'
@@include src/awk/dayview.awk
EOF
)
export AWK_DAYVIEW
AWK_GET=$(
cat <<'EOF'
@@include src/awk/get.awk
EOF
)
export AWK_GET
AWK_UPDATE=$(
cat <<'EOF'
@@include src/awk/update.awk
EOF
)
export AWK_UPDATE
AWK_NEW=$(
cat <<'EOF'
@@include src/awk/new.awk
EOF
)
export AWK_NEW
### END OF AWK SCRIPTS
## Colors
export GREEN="\033[1;32m"
export RED="\033[1;31m"
export WHITE="\033[1;97m"
export CYAN="\033[1;36m"
export ITALIC="\033[3m"
export FAINT="\033[2m"
export OFF="\033[m"
export FZF_VJOUR_USE_EXPORTED="yes"
fi
__load_approx_data() {
find "$ROOT" -type f -name '*.ics' -print0 |
xargs -0 -P0 \
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_LINES"
}
__load_weeks() {
dates=$(awk -F'|' '{ print $2; print $3 }' "$APPROX_DATA_FILE")
file_dates=$(mktemp)
echo "$dates" | date --file="/dev/stdin" +"%s" >"$file_dates"
awk "$AWK_MERGE" "$file_dates" "$APPROX_DATA_FILE"
rm "$file_dates"
}
__show_day() {
weeknr=$(date -d "$DISPLAY_DATE" +"%s")
weeknr=$(((weeknr - 259200) / 604800)) # shift, because epoch origin is a Thursday
files=$(grep "^$weeknr " "$WEEKLY_DATA_FILE" | cut -d " " -f 2-)
# Find relevant files in list of week files
sef=$({
set -- $files
for file in "$@"; do
file="$ROOT/$file"
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_PARSE" "$file"
done
})
if [ -n "$sef" ]; then
today=$(date -d "$DISPLAY_DATE" +"%D")
sef=$(echo "$sef" | while IFS= read -r line; do
set -- $line
starttime="$1"
shift
endtime="$1"
shift
fpath="$(echo "$1" | sed 's/|/ /g')" # we will use | as delimiter (need to convert back!)
shift
description="$(echo "$*" | sed 's/|/:/g')" # we will use | as delimiter
#
daystart=$(date -d "$today 00:00:00" +"%s")
dayend=$(date -d "$today 23:59:59" +"%s")
line=""
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
s=$(date -d "@$starttime" +"%R")
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
s="00:00"
else
continue
fi
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
e=$(date -d "@$endtime" +"%R")
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
e="00:00"
else
continue
fi
echo "$s|$e|$starttime|$endtime|$fpath|$description"
done)
fi
echo "$sef" | sort -n | awk -v daystart="$DAY_START" -v dayend="$DAY_END" "$AWK_DAYVIEW"
}
__list() {
weeknr=$(date -d "$DISPLAY_DATE" +"%s")
weeknr=$(((weeknr - 259200) / 604800)) # shift, because epoch origin is a Thursday
files=$(grep "^$weeknr " "$WEEKLY_DATA_FILE" | cut -d " " -f 2-)
dayofweek=$(date -d "$DISPLAY_DATE" +"%u")
delta=$((1 - dayofweek))
startofweek=$(date -d "$DISPLAY_DATE -$delta days" +"%D")
# loop over files
sef=$({
set -- $files
for file in "$@"; do
file="$ROOT/$file"
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_PARSE" "$file"
done
})
if [ -n "$sef" ]; then
sef=$(echo "$sef" | while IFS= read -r line; do
set -- $line
starttime="$1"
shift
endtime="$1"
shift
#fpath="$1"
shift
description="$*"
for i in $(seq 0 7); do
daystart=$(date -d "$startofweek +$i days 00:00:00" +"%s")
dayend=$(date -d "$startofweek +$i days 23:59:59" +"%s")
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
s=$(date -d "@$starttime" +"%H:%M")
s="$s -"
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
s="00:00 -"
else
continue
fi
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
e=$(date -d "@$endtime" +"%H:%M")
e="- $e"
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
e="- 00:00"
else
continue
fi
echo "$i $s$e >$description"
done
done)
fi
sef=$({
echo "$sef"
seq 0 7
} | sort -n)
echo "$sef" | awk -v startofweek="$startofweek" "$AWK_WEEKVIEW"
#seq -f "$startofweek +%g days" 0 6 |
# LC_ALL=c xargs -I {} date -d "{}" +"%a %e %b %Y"
}
__canonical_datetime_hm() {
s="$1"
t=$(date -d "@$s" +"%R")
dfmt="%F"
if [ "$t" != "00:00" ]; then
dfmt="$dfmt %R"
fi
date -d "@$s" +"$dfmt"
}
__canonical_datetime() {
s="$1"
shift
t=$(date -d "@$s" +"%R")
dfmt="$*%e %b %Y"
if [ "$t" != "00:00" ]; then
dfmt="$dfmt %R %Z"
fi
date -d "@$s" +"$dfmt"
}
__edit() {
start=$(__canonical_datetime_hm "$1")
end=$(__canonical_datetime_hm "$2")
fpath="$3"
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$fpath")
description=$(awk -v field="DESCRIPTION" "$AWK_GET" "$fpath")
filetmp=$(mktemp --suffix='.md')
(
echo "::: |> $start"
echo "::: <| $end"
echo "# $summary"
echo ""
echo "$description"
) >"$filetmp"
checksum=$(cksum "$filetmp")
$EDITOR "$filetmp" >/dev/tty
# Update only if changes are detected
if [ "$checksum" != "$(cksum "$filetmp")" ]; then
filenew="$filetmp.ics"
awk "$AWK_UPDATE" "$filetmp" "$fpath" >"$filenew"
mv "$filenew" "$fpath"
fi
rm "$filetmp"
}
if [ -z "${APPROX_DATA_FILE:-}" ]; then
APPROX_DATA_FILE=$(mktemp)
__load_approx_data >"$APPROX_DATA_FILE"
export APPROX_DATA_FILE
fi
if [ -z "${WEEKLY_DATA_FILE:-}" ]; then
WEEKLY_DATA_FILE=$(mktemp)
__load_weeks >"$WEEKLY_DATA_FILE"
export WEEKLY_DATA_FILE
fi
if [ "${1:-}" = "--new" ]; then
collection=$(echo "$COLLECTION_LABELS" | tr ';' '\n' | $FZF --delimiter='=' --with-nth=2 --accept-nth=1)
fpath=""
while [ -f "$fpath" ] || [ -z "$fpath" ]; do
uuid=$($UUIDGEN)
fpath="$ROOT/$collection/$uuid.ics"
done
startsec=$(date -d "$2" +"%s")
endsec=$((startsec + 3600))
start=$(__canonical_datetime_hm "$startsec")
end=$(__canonical_datetime_hm "$endsec")
filetmp=$(mktemp --suffix='.md')
(
echo "::: |> $start"
echo "::: <| $end"
echo "# <!-- write summary here -->"
echo ""
) >"$filetmp"
checksum=$(cksum "$filetmp")
$EDITOR "$filetmp" >/dev/tty
# Update only if changes are detected
if [ "$checksum" != "$(cksum "$filetmp")" ]; then
filenew="$filetmp.ics"
awk -v uid="$uuid" "$AWK_NEW" "$filetmp" >"$filenew"
mv "$filenew" "$fpath"
fi
rm "$filetmp"
fi
if [ "${1:-}" = "--preview" ]; then
hour=$(echo "$2" | cut -d '|' -f 1)
start=$(echo "$2" | cut -d '|' -f 2)
end=$(echo "$2" | cut -d '|' -f 3)
fpath=$(echo "$2" | cut -d '|' -f 4 | sed "s/ /|/g")
if [ -n "$hour" ] && [ -n "$fpath" ]; then
fpath="$ROOT/$fpath"
start=$(__canonical_datetime "$start" "%a ")
end=$(__canonical_datetime "$end" "%a ")
echo "${GREEN}From: ${OFF}${CYAN}$start${OFF}"
echo "${GREEN}To: ${OFF}${CYAN}$end${OFF}"
echo ""
awk -v field="DESCRIPTION" "$AWK_GET" "$fpath" | $CAT
fi
exit
fi
if [ "${1:-}" = "--day-reload" ]; then
__show_day
exit
fi
if [ "${1:-}" = "--day" ]; then
DISPLAY_DATE="$2"
selection=$(
__show_day |
$FZF \
--reverse \
--ansi \
--no-sort \
--no-input \
--margin='20%' \
--border='double' \
--border-label="🗓️ $(date -d "$DISPLAY_DATE" +"%A %e %B %Y")" \
--color=label:bold:green \
--border-label-pos=3 \
--delimiter='|' \
--with-nth='{5}' \
--accept-nth='1,2,3,4' \
--preview="$0 --preview {}" \
--expect="ctrl-n" \
--bind="ctrl-s:execute($SYNC_CMD ; printf 'Press <enter> to continue.'; read -r tmp)"
)
key=$(echo "$selection" | head -1)
line=$(echo "$selection" | tail -1)
hour=$(echo "$line" | cut -d '|' -f 1)
start=$(echo "$line" | cut -d '|' -f 2)
end=$(echo "$line" | cut -d '|' -f 3)
fpath=$(echo "$line" | cut -d '|' -f 4 | sed "s/ /|/g")
if [ "$key" = "ctrl-n" ]; then
if echo "$hour" | grep ":"; then
hour="$DAY_START"
fi
exec $0 --new "$DISPLAY_DATE $hour:00"
elif [ -n "$fpath" ]; then
fpath="$ROOT/$fpath"
__edit "$start" "$end" "$fpath"
fi
fi
if [ "${1:-}" = "--date" ]; then
DISPLAY_DATE="$2"
fi
DISPLAY_DATE=${DISPLAY_DATE:-today}
DISPLAY_DATE=$(date -d "$DISPLAY_DATE" +"%D")
DISPLAY_POS=$((8 - $(date -d "$DISPLAY_DATE" +"%u")))
DISPLAY_DATE_PREV=$(date -d "$DISPLAY_DATE -1 week" +"%D")
DISPLAY_DATE_NEXT=$(date -d "$DISPLAY_DATE +1 week" +"%D")
DISPLAY_DATE_PREV_MONTH=$(date -d "$DISPLAY_DATE -1 month" +"%D")
DISPLAY_DATE_NEXT_MONTH=$(date -d "$DISPLAY_DATE +1 month" +"%D")
selection=$(
(
cat "$APPROX_DATA_FILE"
yes " " | head -n 50
__list
) |
$FZF \
--tac \
--no-sort \
--no-hscroll \
--ellipsis='' \
--delimiter='|' \
--with-nth='{4}' \
--accept-nth=1,2 \
--no-info \
--ansi \
--no-clear \
--no-scrollbar \
--expect="ctrl-n" \
--bind="load:pos($DISPLAY_POS)" \
--bind="ctrl-u:become($0 --date '$DISPLAY_DATE_PREV')" \
--bind="ctrl-d:become($0 --date '$DISPLAY_DATE_NEXT')" \
--bind="ctrl-alt-u:become($0 --date '$DISPLAY_DATE_PREV_MONTH')" \
--bind="ctrl-alt-d:become($0 --date '$DISPLAY_DATE_NEXT_MONTH')" \
--bind="ctrl-l:become($0)"
)
key=$(echo "$selection" | head -1)
line=$(echo "$selection" | tail -1)
sign=$(echo "$line" | cut -d '|' -f 1)
startdate=$(echo "$line" | cut -d '|' -f 2)
if [ "$key" = "ctrl-n" ]; then
# Add new
exec $0 --new "$startdate $DAY_START:00"
fi
if [ -z "$key" ] && [ -z "$line" ]; then
rm "$WEEKLY_DATA_FILE" "$APPROX_DATA_FILE"
return 0
fi
if [ "$sign" = "~" ]; then
exec $0 --date "$startdate"
else
exec $0 --day "$startdate"
fi
echo "Going to end..."
echo "$selection"
echo "STOPPING NOW"