feat: Basic attachment support

This commit is contained in:
Ämin Baumeler 2025-06-17 08:38:15 +02:00
parent cb84445159
commit c39c45a23a
6 changed files with 280 additions and 2 deletions

35
src/awk/attach.awk Normal file
View File

@ -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 <file) {
line = line aline
if (fl && length(line) >= 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 }

7
src/awk/attachdd.awk Normal file
View File

@ -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 }

46
src/awk/attachls.awk Normal file
View File

@ -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 }

13
src/awk/attachrm.awk Normal file
View File

@ -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 }

10
src/awk/has.awk Normal file
View File

@ -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 }

View File

@ -112,6 +112,7 @@ datetime_str() {
# @input $1: Line from day view containing an event # @input $1: Line from day view containing an event
# @req $ROOT: Path that contains the collections (see configuration) # @req $ROOT: Path that contains the collections (see configuration)
# @req $AWK_GET: Awk script to extract fields from iCalendar file # @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 $CAT: Program to print
# @req colors # @req colors
if [ "${1:-}" = "--preview-event" ]; then if [ "${1:-}" = "--preview-event" ]; then
@ -134,6 +135,10 @@ if [ "${1:-}" = "--preview-event" ]; then
if [ -n "${location:-}" ]; then if [ -n "${location:-}" ]; then
echo "📍 ${CYAN}$location${OFF}" echo "📍 ${CYAN}$location${OFF}"
fi fi
attcnt=$(awk "$AWK_ATTACHLS" "$fpath" | wc -l)
if [ "$attcnt" -gt 0 ]; then
echo "🔗 $attcnt attachments"
fi
echo "" echo ""
awk -v field="DESCRIPTION" "$AWK_GET" "$fpath" | $CAT awk -v field="DESCRIPTION" "$AWK_GET" "$fpath" | $CAT
fi fi
@ -428,6 +433,7 @@ if [ ! -d "$ZI_DIR" ]; then
err "Could not determine time-zone information" err "Could not determine time-zone information"
exit 1 exit 1
fi fi
OPEN=${OPEN:-open}
### ###
### Check and load required tools ### Check and load required tools
@ -478,6 +484,10 @@ fi
### AWK_SET: Set value of specific field in iCalendar file ### AWK_SET: Set value of specific field in iCalendar file
### AWK_UPDATE: Update iCalendar file ### AWK_UPDATE: Update iCalendar file
### AWK_WEEKVIEW: Generate view of the week ### 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 # TODO: Complete documentation
@ -547,6 +557,30 @@ AWK_SET=$(
EOF 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 ### Colors
### ###
@ -652,6 +686,7 @@ __summary_for_commit() {
### __import_to_collection ### __import_to_collection
### __cancel_toggle ### __cancel_toggle
### __tentative_toggle ### __tentative_toggle
### __add_attachment
# __edit() # __edit()
# Edit iCalendar file. # Edit iCalendar file.
@ -853,6 +888,7 @@ __cancel_toggle() {
# __tentative_toggle # __tentative_toggle
# Toggle status flag: CONFIRMED <-> TENTATIVE # Toggle status flag: CONFIRMED <-> TENTATIVE
#
# @input $1: path to iCalendar file # @input $1: path to iCalendar file
# @req $ROOT: Path that contains the collections (see configuration) # @req $ROOT: Path that contains the collections (see configuration)
# @req $AWK_SET: Awk script to set field value # @req $AWK_SET: Awk script to set field value
@ -873,6 +909,32 @@ __tentative_toggle() {
fi 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 ### Extra command-line options
### --import-ni ### --import-ni
@ -1022,7 +1084,7 @@ __refresh_data
### Exports ### Exports
# The preview calls run in subprocesses. These require the following variables: # 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 # The reload commands also run in subprocesses, and use in addition
export COLLECTION_LABELS DAY_START DAY_END AWK_DAYVIEW AWK_WEEKVIEW AWK_PARSE 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 # as well as the following variables that will be dynamically specified. So, we
@ -1132,7 +1194,7 @@ while true; do
--with-nth='{6}' \ --with-nth='{6}' \
--accept-nth='1,2,3,4,5' \ --accept-nth='1,2,3,4,5' \
--preview="$0 --preview-event {}" \ --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( --bind="load:pos(1)+transform(
echo change-border-label:🗓️ \$(date -d {1} +\"%A %e %B %Y\") echo change-border-label:🗓️ \$(date -d {1} +\"%A %e %B %Y\")
)+transform( )+transform(
@ -1192,6 +1254,111 @@ while true; do
__cancel_toggle "$fpath" __cancel_toggle "$fpath"
elif [ "$key" = "c" ] && [ -f "$ROOT/$fpath" ]; then elif [ "$key" = "c" ] && [ -f "$ROOT/$fpath" ]; then
__tentative_toggle "$fpath" __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: <enter> open, <ctrl-alt-d> delete, <shift-a> 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 <enter> 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 elif [ -z "$key" ] && [ -n "$fpath" ]; then
__edit "$start" "$end" "$fpath" __edit "$start" "$end" "$fpath"
set -- "--day" "$DISPLAY_DATE" set -- "--day" "$DISPLAY_DATE"