#!/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 "# " 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 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"