feat: inline attachment support

This commit is contained in:
2025-07-03 13:15:44 +02:00
parent c8642343e7
commit 31c1357fbb
11 changed files with 337 additions and 3 deletions

View File

@@ -86,6 +86,7 @@ In addition, there are the following keybindings:
| `ctrl-x` | Toggle task completion |
| `alt-up` | Increase task priority |
| `alt-down` | Decrease task priority |
| `ctrl-a` | Open attachments view |
| `alt-v` | View bare iCalendar file |
| `alt-0` | Default view: Journal, notes, and _open_ tasks |
| `alt-1` | Display journal entries |
@@ -94,6 +95,16 @@ In addition, there are the following keybindings:
You may also invoke the script with `--help` to see further command-line options.
In the attachment view, you may use the following keys:
| Key | Action |
| --- | ------ |
| `enter` | Open attachment |
| `j` | Down |
| `k` | Up |
| `w` | Toggle line wrap |
| `ctrl-a` | Add attachment |
| `ctrl-alt-d` | Delete attachment |
Git support
-----------
You can track your entries with `git` by simply running `fzf-vjour --git-init`.
@@ -123,7 +134,7 @@ Here is a list of some currently present limitations.
- Timezone agnostic: Timezone specifications are ignored.
- Time agnostic: We use the date portion only of date-time specifications.
- No alarms or notifications
- No attachments
- Inline attachments only
- No recurrences
License

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:(VTODO|VJOURNAL)$/ { write_attachment() }
{ print }

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

@@ -0,0 +1,8 @@
BEGIN { FS="[:;]" }
/^END:(VTODO|VJOURNAL)$/ { ins = 0; exit }
/^[^ ]/ && a { a = 0 }
/^ / && a && p { print substr($0, 2); }
/^ / && a && !p { if (index($0, ":")) { p = 1; print substr($0, index($0, ":")+1) } }
/^ATTACH/ && ins { i++; }
/^ATTACH/ && ins && i == id { a = 1; if (index($0, ":")) { p = 1; print substr($0, index($0, ":")+1) } }
/^BEGIN:(VTODO|VJOURNAL)$/ { ins = 1 }

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

@@ -0,0 +1,41 @@
# 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)
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 (enc)
info = "inline"
}
print i, fin, type, enc, info
}
BEGIN { FS="[:;]"; OFS="\t" }
/^END:(VTODO|VJOURNAL)$/ { 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:(VTODO|VJOURNAL)$/ { 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:(VTODO|VJOURNAL)$/ { ins = 0 }
/^[^ ]/ && a { a = 0 }
/^ / && a { next }
/^ATTACH/ && ins { i++; }
/^ATTACH/ && ins && i == id { a = 1; next }
/^BEGIN:(VTODO|VJOURNAL)$/ { ins = 1 }
{ print }

View File

@@ -50,6 +50,7 @@ BEGIN {
# flag_journal: symbol for journal entries
# flag_note: symbol for note entries
# flag_priority symbol for prior. task
# flag_attachment symbol for attachment
# style_collection
# style_date
# style_summary
@@ -76,6 +77,7 @@ BEGIN {
BEGINFILE {
type = "";
prop = "";
att = "";
delete c;
}
@@ -92,6 +94,11 @@ BEGINFILE {
c[prop] = $0;
next;
}
/^ATTACH/ {
prop = ""
att = 1;
next;
}
/^[^ ]/ && prop {
prop = "";
next;
@@ -193,6 +200,9 @@ ENDFILE {
# categories
categories = cat ? cat : " "
# attachments
att = att ? flag_attachment " " : ""
# filename
# FILENAME
@@ -203,6 +213,6 @@ ENDFILE {
collection,
datecolor d OFF,
flag,
priotext summarycolor summary OFF,
priotext att summarycolor summary OFF,
style_category categories OFF;
}

View File

@@ -23,6 +23,7 @@ __lines() {
-v flag_journal="$FLAG_JOURNAL" \
-v flag_note="$FLAG_NOTE" \
-v flag_priority="$FLAG_PRIORITY" \
-v flag_attachment="$FLAG_ATTACHMENT" \
-v style_collection="$STYLE_COLLECTION" \
-v style_date="$STYLE_DATE" \
-v style_summary="$STYLE_SUMMARY" \
@@ -66,6 +67,9 @@ fi
# Command line arguments: Interal use
. "sh/cli.sh"
# Attachment handling
. "sh/attachment.sh"
while true; do
query=$(stripws "$query")
selection=$(
@@ -77,7 +81,7 @@ while true; do
--print-query \
--accept-nth=4 \
--preview="$0 --preview {4}" \
--expect="ctrl-n,ctrl-alt-d,alt-v" \
--expect="ctrl-n,ctrl-alt-d,alt-v,ctrl-a" \
--bind="ctrl-r:reload($0 --reload)" \
--bind="ctrl-x:reload($0 --reload --toggle-completed {4})" \
--bind="alt-up:reload($0 --reload --change-priority '+1' {4})" \
@@ -116,6 +120,9 @@ while true; do
"alt-v")
$EDITOR "$file"
;;
"ctrl-a")
__attachment_view "$file"
;;
*)
__edit "$file"
;;

178
src/sh/attachment.sh Normal file
View File

@@ -0,0 +1,178 @@
# Add attachment to iCalendar file
#
# @input $1: Path to iCalendar file
__add_attachment() {
file="$1"
shift
sel=$(
$FZF --prompt="Select attachment> " \
--walker="file,hidden" \
--walker-root="$HOME" \
--expect="ctrl-c,ctrl-g,ctrl-q,esc"
)
key=$(echo "$sel" | head -1)
f=$(echo "$sel" | tail -1)
if [ -n "$key" ]; then
f=""
fi
if [ -z "$f" ] || [ ! -f "$f" ]; then
return
fi
filename=$(basename "$f")
mime=$(file -b -i "$f" | cut -d ';' -f 1)
if [ -z "$mime" ]; then
mime="application/octet-stream"
fi
fenc=$(mktemp)
base64 "$f" >"$fenc"
filetmp=$(mktemp)
awk -v file="$fenc" -v mime="$mime" -v filename="$filename" "$AWK_ATTACH" "$file" >"$filetmp"
mv "$filetmp" "$file"
if [ -n "${GIT:-}" ]; then
$GIT add "$file"
$GIT commit -q -m "Added attachment" -- "$file"
fi
rm "$fenc"
}
# Open attachment from iCalendar file
#
# @input $1: Attachment id
# @input $2: Attachment name
# @input $3: Attachment format
# @input $4: Attachment encoding
# @input $5: Path to iCalendar file
__open_attachment() {
attid="$1"
shift
attname="$1"
shift
attfmt="$1"
shift
attenc="$1"
shift
file="$1"
shift
if [ "$attenc" != "base64" ]; then
err "Unsupported attachment encoding: $attenc. Press <enter> to continue."
read -r tmp
return
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 decode
awk -v id="$attid" "$AWK_ATTACHDD" "$file" | 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"
printf "Press <enter> to continue." >/dev/tty
read -r tmp
break
;;
"no")
break
;;
*)
echo "Please answer \"yes\" or \"no\"." >/dev/tty
;;
esac
done
# Clean up
rm -f "$attpath"
if [ -n "${tmpdir:-}" ] && [ -d "${tmpdir:-}" ]; then
rm -rf "$tmpdir"
fi
}
# Delete attachment from iCalendar file
#
# @input $1: Attachment id
# @input $2: Path to iCalendar File
__del_attachment() {
attid="$1"
shift
file="$1"
shift
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" "$file" >"$filetmp"
mv "$filetmp" "$file"
if [ -n "${GIT:-}" ]; then
$GIT add "$file"
$GIT commit -q -m "Deleted attachment" -- "$file"
fi
break
;;
"no")
break
;;
*)
echo "Please answer \"yes\" or \"no\"." >/dev/tty
;;
esac
done
}
# Show attachment window
#
# @input $1: Path to iCalendar file
__attachment_view() {
file="$1"
shift
att=$(
awk "$AWK_ATTACHLS" "$file" |
$FZF \
--delimiter="\t" \
--accept-nth=1,2,3,4 \
--with-nth="Attachment {1}: \"{2}\" {3} ({5})" \
--no-sort \
--tac \
--margin="30%,30%" \
--border=bold \
--border-label="Attachment View Keys: <enter> open, <ctrl-alt-d> delete, <ctrl-a> add" \
--expect="ctrl-a" \
--expect="ctrl-c,ctrl-g,ctrl-q,ctrl-d,esc,q,backspace" \
--print-query \
--bind="start:hide-input" \
--bind="ctrl-alt-d:show-input+change-query(ctrl-alt-d)+accept" \
--bind='load:transform:[ "$FZF_TOTAL_COUNT" -eq 0 ] && echo "unbind(enter)+unbind(ctrl-alt-d)"' \
--bind="w:toggle-wrap" \
--bind="j:down" \
--bind="k:up" ||
true
)
key=$(echo "$att" | head -2 | xargs)
sel=$(echo "$att" | tail -1)
attid=$(echo "$sel" | cut -f 1)
attname=$(echo "$sel" | cut -f 2)
attfmt=$(echo "$sel" | cut -f 3)
attenc=$(echo "$sel" | cut -f 4)
case "$key" in
"ctrl-c" | "ctrl-g" | "ctrl-q" | "ctrl-d" | "esc" | "q" | "backspace") ;;
"ctrl-alt-d")
__del_attachment "$attid" "$file"
;;
"ctrl-a")
__add_attachment "$file"
;;
*)
__open_attachment "$attid" "$attname" "$attfmt" "$attenc" "$file"
;;
esac
#
}

View File

@@ -39,3 +39,31 @@ AWK_UPDATE=$(
EOF
)
export AWK_UPDATE
AWK_ATTACH=$(
cat <<'EOF'
@@include awk/attach.awk
EOF
)
export AWK_ATTACH
AWK_ATTACHDD=$(
cat <<'EOF'
@@include awk/attachdd.awk
EOF
)
export AWK_ATTACHDD
AWK_ATTACHLS=$(
cat <<'EOF'
@@include awk/attachls.awk
EOF
)
export AWK_ATTACHLS
AWK_ATTACHRM=$(
cat <<'EOF'
@@include awk/attachrm.awk
EOF
)
export AWK_ATTACHRM

View File

@@ -43,3 +43,5 @@ if command -v "git" >/dev/null && [ -d "$ROOT/.git" ]; then
GIT="git -C $ROOT"
export GIT
fi
export OPEN=${OPEN:-open}

View File

@@ -11,6 +11,7 @@ export FLAG_COMPLETED="${FLAG_COMPLETED:-✅}"
export FLAG_JOURNAL="${FLAG_JOURNAL:-📘}"
export FLAG_NOTE="${FLAG_NOTE:-🗒️}"
export FLAG_PRIORITY="${FLAG_PRIORITY:-}"
export FLAG_ATTACHMENT="${FLAG_ATTACHMENT:-🔗}"
# Style
export STYLE_COLLECTION="${STYLE_COLLECTION:-$FAINT$WHITE}"