From c39c45a23a1a18b44dfcbd4488cee0f160241953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84min=20Baumeler?= Date: Tue, 17 Jun 2025 08:38:15 +0200 Subject: [PATCH] feat: Basic attachment support --- src/awk/attach.awk | 35 +++++++++ src/awk/attachdd.awk | 7 ++ src/awk/attachls.awk | 46 ++++++++++++ src/awk/attachrm.awk | 13 ++++ src/awk/has.awk | 10 +++ src/main.sh | 171 ++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 src/awk/attach.awk create mode 100644 src/awk/attachdd.awk create mode 100644 src/awk/attachls.awk create mode 100644 src/awk/attachrm.awk create mode 100644 src/awk/has.awk diff --git a/src/awk/attach.awk b/src/awk/attach.awk new file mode 100644 index 0000000..4ab6599 --- /dev/null +++ b/src/awk/attach.awk @@ -0,0 +1,35 @@ +## src/awk/attach.awk +## Prepend attachment to iCalendar file. +## +## @assign file: Path to base64-encoded content +## @assign mime: Mime +## @assign filename: Original filename + +# Functions + +# Write attachment +# +# @local variables: line, aline +function write_attachment( line, aline, fl) { + line = "ATTACH;ENCODING=BASE64;VALUE=BINARY;FMTTYPE="mime";FILENAME="filename":" + fl = 1 + while (getline aline = 73) { + print substr(line, 1, 73) + line = substr(line, 74) + fl = 0 + } + while (length(line) >= 72) { + print " "substr(line, 1, 72) + line = substr(line, 73) + } + } + if (line) + print " "line +} + +# AWK program + +/^END:VEVENT$/ { write_attachment() } +{ print } diff --git a/src/awk/attachdd.awk b/src/awk/attachdd.awk new file mode 100644 index 0000000..bb7f000 --- /dev/null +++ b/src/awk/attachdd.awk @@ -0,0 +1,7 @@ +BEGIN { FS="[:;]" } +/^END:VEVENT$/ { ins = 0; exit } +/^[^ ]/ && a { a = 0 } +/^ / && a { print substr($0, 2) } +/^ATTACH/ && ins { i++; } +/^ATTACH/ && ins && i == id { a = 1; print substr($0, index($0, ":")+1) } +/^BEGIN:VEVENT$/ { ins = 1 } diff --git a/src/awk/attachls.awk b/src/awk/attachls.awk new file mode 100644 index 0000000..f03c36d --- /dev/null +++ b/src/awk/attachls.awk @@ -0,0 +1,46 @@ +# Decide if we need to read more to get all properties +# +# @input str: strin read so far +# @return: 1 if we need more data, 0 otherwise +function cont_reading(str) { + return index(str, ":") ? 0 : 1 +} + +# Get information about attachment +# +# @input i: Attachment index +# @input str: Attachment string (at least up to content separator `:`) +# @return: informative string +function att_info(i, str, cnt, k, info) { + str = substr(str, 1, index(str, ":") - 1) + cnt = split(str, props) + info = "Attachment "i":" + if (cnt > 1) { + for (k=2; k<=cnt; k++) { + pname = substr(props[k], 1, index(props[k], "=") - 1) + pvalu = substr(props[k], index(props[k], "=") + 1) + if (pname == "ENCODING" && pvalu = "BASE64") + enc = "base64" + if (pname == "FILENAME") + fin = pvalu + if (pname == "VALUE") + val = pvalu + if (pname == "FMTTYPE") + type = pvalu + } + if (fin) + info = info " \"" fin "\"" + if (type) + info = info " " type + if (enc) + info = info " (inline)" + } + print i, fin, type, enc, info +} + +BEGIN { FS="[:;]"; OFS="\t" } +/^END:VEVENT$/ { ins = 0; exit } +l && !r { att_info(i, l); l = "" } +/^ / && r { l = l substr($0, 2); r = cont_reading($0) } +/^ATTACH/ && ins { i++; l = $0; r = cont_reading($0) } +/^BEGIN:VEVENT$/ { ins = 1 } diff --git a/src/awk/attachrm.awk b/src/awk/attachrm.awk new file mode 100644 index 0000000..d584f92 --- /dev/null +++ b/src/awk/attachrm.awk @@ -0,0 +1,13 @@ +## src/awk/attachrm.awk +## Remove attachment from iCalendar file. +## +## @assign id: Attachment number to remove + +BEGIN { FS="[:;]" } +/^END:VEVENT$/ { ins = 0 } +/^[^ ]/ && a { a = 0 } +/^ / && a { next } +/^ATTACH/ && ins { i++; } +/^ATTACH/ && ins && i == id { a = 1; next } +/^BEGIN:VEVENT$/ { ins = 1 } +{ print } diff --git a/src/awk/has.awk b/src/awk/has.awk new file mode 100644 index 0000000..d310931 --- /dev/null +++ b/src/awk/has.awk @@ -0,0 +1,10 @@ +## src/awk/has.awk +## Decide if VEVENT file has a specific field. +## +## @assign field: Field name + +# AWK program +BEGIN { FS = "[:;]" } +/^BEGIN:VEVENT$/ { ins = 1 } +/^END:VEVENT$/ { exit 1 } +ins && $1 == field { exit 0 } diff --git a/src/main.sh b/src/main.sh index 0db0249..7a3c1c4 100755 --- a/src/main.sh +++ b/src/main.sh @@ -112,6 +112,7 @@ datetime_str() { # @input $1: Line from day view containing an event # @req $ROOT: Path that contains the collections (see configuration) # @req $AWK_GET: Awk script to extract fields from iCalendar file +# @req $AWK_ATTACHLS: Awk script to list attachments # @req $CAT: Program to print # @req colors if [ "${1:-}" = "--preview-event" ]; then @@ -134,6 +135,10 @@ if [ "${1:-}" = "--preview-event" ]; then if [ -n "${location:-}" ]; then echo "📍 ${CYAN}$location${OFF}" fi + attcnt=$(awk "$AWK_ATTACHLS" "$fpath" | wc -l) + if [ "$attcnt" -gt 0 ]; then + echo "🔗 $attcnt attachments" + fi echo "" awk -v field="DESCRIPTION" "$AWK_GET" "$fpath" | $CAT fi @@ -428,6 +433,7 @@ if [ ! -d "$ZI_DIR" ]; then err "Could not determine time-zone information" exit 1 fi +OPEN=${OPEN:-open} ### ### Check and load required tools @@ -478,6 +484,10 @@ fi ### AWK_SET: Set value of specific field in iCalendar file ### AWK_UPDATE: Update iCalendar file ### AWK_WEEKVIEW: Generate view of the week +### AWK_ATTACHLS: List attachments +### AWK_ATTACHDD: Store attachment +### AWK_ATTACHRM: Remove attachment +### AWK_ATTACH: Add attachment ### # TODO: Complete documentation @@ -547,6 +557,30 @@ AWK_SET=$( EOF ) +AWK_ATTACHLS=$( + cat <<'EOF' +@@include src/awk/attachls.awk +EOF +) + +AWK_ATTACHDD=$( + cat <<'EOF' +@@include src/awk/attachdd.awk +EOF +) + +AWK_ATTACHRM=$( + cat <<'EOF' +@@include src/awk/attachrm.awk +EOF +) + +AWK_ATTACH=$( + cat <<'EOF' +@@include src/awk/attach.awk +EOF +) + ### ### Colors ### @@ -652,6 +686,7 @@ __summary_for_commit() { ### __import_to_collection ### __cancel_toggle ### __tentative_toggle +### __add_attachment # __edit() # Edit iCalendar file. @@ -853,6 +888,7 @@ __cancel_toggle() { # __tentative_toggle # Toggle status flag: CONFIRMED <-> TENTATIVE +# # @input $1: path to iCalendar file # @req $ROOT: Path that contains the collections (see configuration) # @req $AWK_SET: Awk script to set field value @@ -873,6 +909,32 @@ __tentative_toggle() { fi } +# __add_attachment +# Prepend attachment to iCalendar file +# +# @input $1: path to iCalendar file +# @req $ROOT: Path that contains the collections (see configuration) +# @req $FZF: Fuzzy finder +# @req $AWK_ATTACH: Awk script to add attachment +__add_attachment() { + fpath="$ROOT/$1" + f=$($FZF --prompt="Select attachment> ") + if [ -z "$f" ] || [ ! -f "$f" ]; then + return + fi + filename=$(basename "$f") + fenc=$(mktemp) + base64 "$f" >"$fenc" + filetmp=$(mktemp) + awk -v file="$fenc" -v mime="application/octet-stream" -v filename="$filename" "$AWK_ATTACH" "$fpath" >"$filetmp" + mv "$filetmp" "$fpath" + if [ -n "${GIT:-}" ]; then + $GIT add "$fpath" + $GIT commit -m "Added attachment to '$(__summary_for_commit "$fpath") ...'" -- "$fpath" + fi + rm "$fenc" +} + ### ### Extra command-line options ### --import-ni @@ -1022,7 +1084,7 @@ __refresh_data ### Exports # The preview calls run in subprocesses. These require the following variables: -export ROOT CAT AWK_GET AWK_CALSHIFT AWK_CALANNOT CYAN STRIKE FAINT WHITE ITALIC OFF +export ROOT CAT AWK_GET AWK_CALSHIFT AWK_CALANNOT CYAN STRIKE FAINT WHITE ITALIC OFF AWK_ATTACHLS # The reload commands also run in subprocesses, and use in addition export COLLECTION_LABELS DAY_START DAY_END AWK_DAYVIEW AWK_WEEKVIEW AWK_PARSE # as well as the following variables that will be dynamically specified. So, we @@ -1132,7 +1194,7 @@ while true; do --with-nth='{6}' \ --accept-nth='1,2,3,4,5' \ --preview="$0 --preview-event {}" \ - --expect="ctrl-n,ctrl-t,ctrl-g,ctrl-alt-d,esc,backspace,q,alt-v,x,c" \ + --expect="ctrl-n,ctrl-t,ctrl-g,ctrl-alt-d,esc,backspace,q,alt-v,x,c,a" \ --bind="load:pos(1)+transform( echo change-border-label:🗓️ \$(date -d {1} +\"%A %e %B %Y\") )+transform( @@ -1192,6 +1254,111 @@ while true; do __cancel_toggle "$fpath" elif [ "$key" = "c" ] && [ -f "$ROOT/$fpath" ]; then __tentative_toggle "$fpath" + elif [ "$key" = "a" ] && [ -f "$ROOT/$fpath" ]; then + echo "GO" >>/tmp/foo + att=$( + ( + echo "1\t2\t3\t4\t5\t6" + awk "$AWK_ATTACHLS" "$ROOT/$fpath" + ) | + $FZF \ + --no-clear \ + --delimiter="\t" \ + --accept-nth=1,2,3,4 \ + --with-nth=5 \ + --no-sort \ + --tac \ + --no-input \ + --margin="30%,30%" \ + --border=bold \ + --border-label='Attachment View Keys: open, delete, add' \ + --expect="ctrl-c,esc,q,backspace,ctrl-alt-d,A" \ + --bind="j:down" \ + --bind="k:up" + ) + echo "GO" >>/tmp/foo + echo "$att" >>/tmp/foo + key=$(echo "$att" | head -1) + sel=$(echo "$att" | tail -1) + if [ "$key" = "ctrl-c" ] || [ "$key" = "esc" ] || [ "$key" = "q" ] || [ "$key" = "backspace" ]; then + continue + fi + if [ "$key" = "A" ]; then + __add_attachment "$fpath" + continue + fi + attid=$(echo "$sel" | cut -f 1) + attname=$(echo "$sel" | cut -f 2) + attfmt=$(echo "$sel" | cut -f 3) + attenc=$(echo "$sel" | cut -f 4) + if [ -z "$attid" ]; then + continue + fi + if [ "$key" = "ctrl-alt-d" ]; then + while true; do + printf "Are you sure you want to delete attachment \"%s\"? (yes/no): " "$attid" >/dev/tty + read -r yn + case $yn in + "yes") + filetmp=$(mktemp) + awk -v id="$attid" "$AWK_ATTACHRM" "$ROOT/$fpath" >"$filetmp" + mv "$filetmp" "$ROOT/$fpath" + if [ -n "${GIT:-}" ]; then + $GIT add "$fpath" + $GIT commit -m "Deleted attachment from event '$(__summary_for_commit "$fpath") ...'" -- "$fpath" + fi + break + ;; + "no") + break + ;; + *) + echo "Please answer \"yes\" or \"no\"." >/dev/tty + ;; + esac + done + continue + fi + if [ "$attenc" != "base64" ]; then + err "Unsupported attachment encoding: $attenc" + read -r tmp + continue + fi + if [ -n "$attname" ]; then + tmpdir=$(mktemp -d) + attpath="$tmpdir/$attname" + elif [ -n "$attfmt" ]; then + attext=$(echo "$attfmt" | cut -d "/" -f 2) + attpath=$(mktemp --suffix="$attext") + else + attpath=$(mktemp) + fi + # Get file and uncode + awk -v id="$attid" "$AWK_ATTACHDD" "$ROOT/$fpath" | base64 -d >"$attpath" + fn=$(file "$attpath") + while true; do + printf "Are you sure you want to open \"%s\"? (yes/no): " "$fn" >/dev/tty + read -r yn + case $yn in + "yes") + $OPEN "$attpath" + break + ;; + "no") + break + ;; + *) + echo "Please answer \"yes\" or \"no\"." >/dev/tty + ;; + esac + done + printf "Press to conintue" >/dev/tty + read -r tmp + # Clean up + rm -f "$attpath" + if [ -n "${tmpdir:-}" ] && [ -d "${tmpdir:-}" ]; then + rm -rf "$tmpdir" + fi elif [ -z "$key" ] && [ -n "$fpath" ]; then __edit "$start" "$end" "$fpath" set -- "--day" "$DISPLAY_DATE"