From 498c7371b7e49d13bcd93315645b9f142f826adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84min=20Baumeler?= Date: Mon, 2 Jun 2025 09:19:28 +0200 Subject: [PATCH] externalize awk scripts --- .gitignore | 1 + fzf-vjour | 48 +++++++--- scripts/build.sh | 11 +++ src/awk/altertodo.awk | 41 ++++++++ src/awk/export.awk | 39 ++++++++ src/awk/get.awk | 18 ++++ src/awk/list.awk | 215 ++++++++++++++++++++++++++++++++++++++++++ src/awk/new.awk | 96 +++++++++++++++++++ src/awk/update.awk | 85 +++++++++++++++++ 9 files changed, 542 insertions(+), 12 deletions(-) create mode 100644 .gitignore create mode 100755 scripts/build.sh create mode 100644 src/awk/altertodo.awk create mode 100644 src/awk/export.awk create mode 100644 src/awk/get.awk create mode 100644 src/awk/list.awk create mode 100644 src/awk/new.awk create mode 100644 src/awk/update.awk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad93b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +fzf-vjour diff --git a/fzf-vjour b/fzf-vjour index 7b7b108..a6f7d9d 100755 --- a/fzf-vjour +++ b/fzf-vjour @@ -11,7 +11,9 @@ if [ -z "$ROOT" ] || [ -z "$SYNC_CMD" ] || [ -z "$COLLECTION_LABELS" ]; then fi ### AWK SCRIPTS -AWK_ALTERTODO='# Increase/decrease priority, or toggle completed status +AWK_ALTERTODO=$( + cat <<'EOF' +# Increase/decrease priority, or toggle completed status # # If `delta` is specified using `-v`, then the priority value is increased by # `delta.` If `delta` is unspecified (or equal to 0), then the completeness @@ -51,9 +53,13 @@ BEGIN { /^PRIORITY:/ && inside { prio = $2; next } /^STATUS/ && inside { status = $2; next } /^PERCENT-COMPLETE/ && inside { next } # ignore, we take STATUS:COMPLETED as reference -{ print }' +{ print } +EOF +) -AWK_EXPORT='function getcontent(content_line, prop) +AWK_EXPORT=$( + cat <<'EOF' +function getcontent(content_line, prop) { return substr(content_line[prop], index(content_line[prop], ":") + 1); } @@ -91,9 +97,13 @@ END { print "> " c["CATEGORIES"]; print ""; print c["DESCRIPTION"]; -}' +} +EOF +) -AWK_GET='# print content of field `field` +AWK_GET=$( + cat <<'EOF' +# print content of field `field` BEGIN { FS = ":"; regex = "^" field; } /^BEGIN:(VJOURNAL|VTODO)/ { type = $2 } /^END:/ && $2 == type { exit } @@ -110,9 +120,13 @@ END { gsub("\\\\;", ";", content); gsub("\\\\\\\\", "\\", content); print content; -}' +} +EOF +) -AWK_LIST='# awk script to generate summary line for iCalendar VJOURNAL and VTODO entries +AWK_LIST=$( + cat <<'EOF' +# awk script to generate summary line for iCalendar VJOURNAL and VTODO entries # # See https://datatracker.ietf.org/doc/html/rfc5545 for the RFC 5545 that # describes iCalendar, and its syntax @@ -326,9 +340,13 @@ ENDFILE { priotext summarycolor summary OFF, WHITE categories OFF, " " FAINT FILENAME OFF; -}' +} +EOF +) -AWK_NEW='function escape_categories(str) +AWK_NEW=$( + cat <<'EOF' +function escape_categories(str) { gsub("\\\\", "\\\\", str); gsub(";", "\\\\;", str); @@ -423,9 +441,13 @@ END { if (desc) print_fold("DESCRIPTION:", desc, i, s); print "END:" type; print "END:VCALENDAR" -}' +} +EOF +) -AWK_UPDATE='function getcontent(content_line, prop) +AWK_UPDATE=$( + cat <<'EOF' +function getcontent(content_line, prop) { return substr(content_line[prop], index(content_line[prop], ":") + 1); } @@ -509,7 +531,9 @@ NR == FNR { print_fold("DESCRIPTION:", desc, i, s); type = ""; } -{ print }' +{ print } +EOF +) ### END OF AWK SCRIPTS __lines() { diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..02be085 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +BOLD="\033[1m" +GREEN="\033[0;32m" +OFF="\033[m" +NAME="fzf-vjour" +SRC="./src/fzf-vjour" +echo "🐔 ${GREEN}Building${OFF} ${BOLD}$NAME${OFF}" +sed -E 's|@@include (.+)$|cat \1|e' "$SRC" >"$NAME" +chmod +x "$NAME" +echo "🥚 ${GREEN}Done${OFF}" diff --git a/src/awk/altertodo.awk b/src/awk/altertodo.awk new file mode 100644 index 0000000..ebb797d --- /dev/null +++ b/src/awk/altertodo.awk @@ -0,0 +1,41 @@ +# Increase/decrease priority, or toggle completed status +# +# If `delta` is specified using `-v`, then the priority value is increased by +# `delta.` If `delta` is unspecified (or equal to 0), then the completeness +# status is toggled. +BEGIN { + FS=":"; + zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1); + delta = delta + 0; # cast as integer +} +/^END:VTODO/ && inside { + # Print sequence and last-modified, if not yet printed + if (!seq) print "SEQUENCE:1"; + if (!lm) print "LAST-MODIFIED:" zulu; + + # Print priority + prio = prio ? prio + delta : 0 + delta; + prio = prio < 0 ? 0 : prio; + prio = prio > 9 ? 9 : prio; + print "PRIORITY:" prio; + + # Print status (toggle if needed) + bit_status = status == "COMPLETED" ? 1 : 0; + bit_toggle = delta ? 0 : 1; + percent = xor(bit_status, bit_toggle) ? 100 : 0; + status = xor(bit_status, bit_toggle) ? "COMPLETED" : "NEEDS-ACTION"; + print "STATUS:" status + print "PERCENT-COMPLETE:" percent + + # print rest + inside = ""; + print $0; + next +} +/^BEGIN:VTODO/ { inside = 1; print; next } +/^SEQUENCE/ && inside { seq = 1; print "SEQUENCE:" $2+1; next } +/^LAST-MODIFIED/ && inside { lm = 1; print "LAST-MODIFIED:" zulu; next } +/^PRIORITY:/ && inside { prio = $2; next } +/^STATUS/ && inside { status = $2; next } +/^PERCENT-COMPLETE/ && inside { next } # ignore, we take STATUS:COMPLETED as reference +{ print } diff --git a/src/awk/export.awk b/src/awk/export.awk new file mode 100644 index 0000000..f81f74f --- /dev/null +++ b/src/awk/export.awk @@ -0,0 +1,39 @@ +function getcontent(content_line, prop) +{ + return substr(content_line[prop], index(content_line[prop], ":") + 1); +} + +function storetext_line(content_line, c, prop) +{ + c[prop] = getcontent(content_line, prop); + gsub("\\\\n", "\n", c[prop]); + gsub("\\\\N", "\n", c[prop]); + gsub("\\\\,", ",", c[prop]); + gsub("\\\\;", ";", c[prop]); + gsub("\\\\\\\\", "\\", c[prop]); +} + +BEGIN { FS = "[:;]"; } +/^BEGIN:(VJOURNAL|VTODO)/ { type = $2 } +/^END:/ && $2 == type { exit } +/^(CATEGORIES|DESCRIPTION|SUMMARY|DUE)/ { prop = $1; content_line[prop] = $0; next; } +/^[^ ]/ && prop { prop = ""; next; } +/^ / && prop { content_line[prop] = content_line[prop] substr($0, 2); next; } + +END { + if (!type) { + exit + } + # Process content lines + storetext_line(content_line, c, "CATEGORIES" ); + storetext_line(content_line, c, "DESCRIPTION"); + storetext_line(content_line, c, "SUMMARY" ); + storetext_line(content_line, c, "DUE" ); + # Print + if (c["DUE"]) + print "::: <| " substr(c["DUE"], 1, 4) "-" substr(c["DUE"], 5, 2) "-" substr(c["DUE"], 7, 2); + print "# " c["SUMMARY"]; + print "> " c["CATEGORIES"]; + print ""; + print c["DESCRIPTION"]; +} diff --git a/src/awk/get.awk b/src/awk/get.awk new file mode 100644 index 0000000..a013736 --- /dev/null +++ b/src/awk/get.awk @@ -0,0 +1,18 @@ +# print content of field `field` +BEGIN { FS = ":"; regex = "^" field; } +/^BEGIN:(VJOURNAL|VTODO)/ { type = $2 } +/^END:/ && $2 == type { exit } +$0 ~ field { content = $0; next; } +/^ / && content { content = content substr($0, 2); next; } +/^[^ ]/ && content { exit } +END { + if (!type) { exit } + # Process content line + content = substr(content, index(content, ":") + 1); + gsub("\\\\n", "\n", content); + gsub("\\\\N", "\n", content); + gsub("\\\\,", ",", content); + gsub("\\\\;", ";", content); + gsub("\\\\\\\\", "\\", content); + print content; +} diff --git a/src/awk/list.awk b/src/awk/list.awk new file mode 100644 index 0000000..cdb7101 --- /dev/null +++ b/src/awk/list.awk @@ -0,0 +1,215 @@ +# awk script to generate summary line for iCalendar VJOURNAL and VTODO entries +# +# See https://datatracker.ietf.org/doc/html/rfc5545 for the RFC 5545 that +# describes iCalendar, and its syntax + +function getcontent(content_line, prop) +{ + return substr(content_line[prop], index(content_line[prop], ":") + 1); +} + +function storetext_line(content_line, c, prop) +{ + c[prop] = getcontent(content_line, prop); + gsub("\\\\n", " ", c[prop]); + gsub("\\\\N", " ", c[prop]); + gsub("\\\\,", ",", c[prop]); + gsub("\\\\;", ";", c[prop]); + gsub("\\\\\\\\", "\\", c[prop]); + #gsub(" ", "_", c[prop]); +} + +function storeinteger(content_line, c, prop) +{ + c[prop] = getcontent(content_line, prop); + c[prop] = c[prop] ? c[prop] : 0; +} + +function storedatetime(content_line, c, prop) +{ + c[prop] = getcontent(content_line, prop); +} + +function storedate(content_line, c, prop) +{ + c[prop] = substr(getcontent(content_line, prop), 1, 8); +} + +function formatdate(date, today, todaystamp, ts, ts_y, ts_m, ts_d, delta) +{ + ts_y = substr(date, 1, 4); + ts_m = substr(date, 5, 2); + ts_d = substr(date, 7); + ts = mktime(ts_y " " ts_m " " ts_d " 00 00 00"); + delta = (ts - todaystamp) / 86400; + if (delta >= 0 && delta < 1) { + return " today"; + } + if (delta >= 1 && delta < 2) { + return " tomorrow"; + } + if (delta >= 2 && delta < 3) { + return " in two days"; + } + if (delta >= 3 && delta < 4) { + return " in three days"; + } + if (delta < 0 && delta >= -1) { + return " yesterday"; + } + if (delta < -1 && delta >= -2) { + return " two days ago"; + } + if (delta < -2 && delta >= -3) { + return "three days ago"; + } + return " " substr(date, 1, 4) "-" substr(date, 5, 2) "-" substr(date, 7); +} + +BEGIN { + # We require the following variables to be set using -v + # collection_lables: ;-delimited collection=label strings + # flag_open: symbol for open to-dos + # flag_completed: symbol for completed to-dos + # flag_journal: symbol for journal entries + # flag_note: symbol for note entries + + FS = "[:;]"; + # Collections + split(collection_labels, mapping, ";"); + for (map in mapping) + { + split(mapping[map], m, "="); + collection2label[m[1]] = m[2]; + } + # Colors + GREEN = "\033[1;32m"; + RED = "\033[1;31m"; + WHITE = "\033[1;97m"; + CYAN = "\033[1;36m"; + FAINT = "\033[2m"; + OFF = "\033[m"; + + # For date comparision + today = strftime("%Y%m%d"); + todaystamp = mktime(substr(today, 1, 4) " " substr(today, 5, 2) " " substr(today, 7) " 00 00 00"); +} + +# Reset variables +BEGINFILE { + type = ""; + prop = ""; + delete content_line; + delete c; + +} + +/^BEGIN:(VJOURNAL|VTODO)/ { + type = $2 +} + +/^END:/ && $2 == type { + nextfile +} + +/^(CATEGORIES|DESCRIPTION|PRIORITY|STATUS|SUMMARY|COMPLETED|DUE|DTSTART|DURATION|CREATED|DTSTAMP|LAST-MODIFIED)/ { + prop = $1; + content_line[prop] = $0; + next; +} +/^[^ ]/ && prop { + prop = ""; + next; +} +/^ / && prop { + content_line[prop] = content_line[prop] substr($0, 2); + next; +} + +ENDFILE { + if (!type) { + exit + } + # Process content lines + storetext_line(content_line, c, "CATEGORIES" ); + storetext_line(content_line, c, "DESCRIPTION" ); + storeinteger( content_line, c, "PRIORITY" ); + storetext_line(content_line, c, "STATUS" ); + storetext_line(content_line, c, "SUMMARY" ); + storedatetime( content_line, c, "COMPLETED" ); + storedate( content_line, c, "DUE" ); + storedate( content_line, c, "DTSTART" ); + storedatetime( content_line, c, "DURATION" ); + storedatetime( content_line, c, "CREATED" ); + storedatetime( content_line, c, "DTSTAMP" ); + storedatetime( content_line, c, "LAST-MODIFIED"); + + # Priority field, primarly used for sorting + priotext = ""; + prio = 0; + if (c["PRIORITY"] > 0) + { + priotext = "❗(" c["PRIORITY"] ") "; + prio = 10 - c["PRIORITY"]; + } + + # Last modification/creation time stamp, used for sorting + # LAST-MODIFIED: Optional field for VTODO and VJOURNAL entries, date-time in + # UTC time format + # DTSTAMP: mandatory field in VTODO and VJOURNAL, date-time in UTC time + # format + mod = c["LAST-MODIFIED"] ? c["LAST-MODIFIED"] : c["DTSTAMP"]; + + # Collection name + depth = split(FILENAME, path, "/"); + collection = depth > 1 ? path[depth-1] : ""; + collection = collection in collection2label ? collection2label[collection] : collection; + + # Date field. For VTODO entries, we show the due date, for journal entries, + # the associated date. + datecolor = CYAN; + summarycolor = GREEN; + + if (type == "VTODO") + { + # Either DUE or DURATION may appear. If DURATION appears, then also DTSTART + d = c["DUE"] ? c["DUE"] : + (c["DURATION"] ? c["DTSTART"] " for " c["DURATION"] : ""); + if (d && d <= today && c["STATUS"] != "COMPLETED") + { + datecolor = RED; + summarycolor = RED; + } + } else { + d = c["DTSTART"]; + } + d = d ? formatdate(d, today, todaystamp ts, ts_y, ts_m, ts_d, delta) : " "; + + # flag: - "journal" for VJOURNAL with DTSTART + # - "note" for VJOURNAL without DTSTART + # - "completed" for VTODO with c["STATUS"] == COMPLETED + # - "open" for VTODO with c["STATUS"] != COMPLETED + if (type == "VTODO") + flag = c["STATUS"] == "COMPLETED" ? flag_completed : flag_open; + else + flag = c["DTSTART"] ? flag_journal : flag_note; + + # summary + # c["SUMMARY"] + summary = c["SUMMARY"] ? c["SUMMARY"] : " " + + # categories + categories = c["CATEGORIES"] ? c["CATEGORIES"] : " " + + # filename + # FILENAME + + print prio, + mod, + collection, + datecolor d OFF, + flag, + priotext summarycolor summary OFF, + WHITE categories OFF, + " " FAINT FILENAME OFF; +} diff --git a/src/awk/new.awk b/src/awk/new.awk new file mode 100644 index 0000000..ad3834d --- /dev/null +++ b/src/awk/new.awk @@ -0,0 +1,96 @@ +function escape_categories(str) +{ + gsub("\\\\", "\\\\", str); + gsub(";", "\\\\;", str); +} + +function escape(str) +{ + escape_categories(str) + gsub(",", "\\\\,", str); +} + +function print_fold(nameparam, content, i, s) +{ + i = 74 - length(nameparam); + s = substr(content, 1, i); + print nameparam s; + s = substr(content, i+1, 73); + i = i + 73; + while (s) + { + print " " s; + s = substr(content, i+1, 73); + i = i + 73; + } +} + +BEGIN { + FS=":"; + type = "VJOURNAL"; + zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1); +} +desc { desc = desc "\\n" $0; next; } +{ + if (substr($0, 1, 6) == "::: |>") + { + start = substr(zulu, 1, 8); + getline; + } + if (substr($0, 1, 6) == "::: <|") + { + type = "VTODO" + due = substr($0, 8); + getline; + } + summary = substr($0, 1, 2) != "# " ? "" : substr($0, 3); + getline; + categories = substr($0, 1, 1) != ">" ? "" : substr($0, 3); + getline; # This line should be empty + getline; # First line of description + desc = $0; + next; +} +END { + # Sanitize input + if (due) { + # Use command line `date` for parsing + cmd = "date -d \"" due "\" +\"%Y%m%d\""; + cmd | getline res + due = res ? res : "" + } + escape(summary); + escape(desc); + escape_categories(categories); + + # print ical + print "BEGIN:VCALENDAR"; + print "VERSION:2.0"; + print "CALSCALE:GREGORIAN"; + print "PRODID:-//fab//awk//EN"; + print "BEGIN:" type; + print "DTSTAMP:" zulu; + print "UID:" uid; + print "CLASS:PRIVATE"; + print "CREATED:" zulu; + print "SEQUENCE:1"; + print "LAST-MODIFIED:" zulu; + if (type == "VTODO") + { + print "STATUS:NEEDS-ACTION"; + print "PERCENT-COMPLETE:0"; + if (due) + print "DUE;VALUE=DATE:" due; + } + else + { + print "STATUS:FINAL"; + if (start) + print "DTSTART;VALUE=DATE:" start; + } + if (summary) print_fold("SUMMARY:", summary, i, s); + if (categories) print_fold("CATEGORIES:", categories, i, s); + if (desc) print_fold("DESCRIPTION:", desc, i, s); + print "END:" type; + print "END:VCALENDAR" +} diff --git a/src/awk/update.awk b/src/awk/update.awk new file mode 100644 index 0000000..ddcff8c --- /dev/null +++ b/src/awk/update.awk @@ -0,0 +1,85 @@ +function getcontent(content_line, prop) +{ + return substr(content_line[prop], index(content_line[prop], ":") + 1); +} + +function escape_categories(str) +{ + gsub("\\\\", "\\\\", str); + gsub(";", "\\\\;", str); +} + +function escape(str) +{ + escape_categories(str) + gsub(",", "\\\\,", str); +} + +function print_fold(nameparam, content, i, s) +{ + i = 74 - length(nameparam); + s = substr(content, 1, i); + print nameparam s; + s = substr(content, i+1, 73); + i = i + 73; + while (s) + { + print " " s; + s = substr(content, i+1, 73); + i = i + 73; + } +} + +BEGIN { + FS=":"; + zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1); +} + +ENDFILE { + if (NR == FNR) + { + # Sanitize input + if (due) { + # Use command line `date` for parsing + cmd = "date -d \"" due "\" +\"%Y%m%d\""; + cmd | getline res + due = res ? res : "" + } + escape(summary); + escape(desc); + escape_categories(categories); + } +} + +NR == FNR && desc { desc = desc "\\n" $0; next; } +NR == FNR { + if (substr($0, 1, 6) == "::: <|") + { + due = substr($0, 8); + getline; + } + summary = substr($0, 1, 2) != "# " ? "" : substr($0, 3); + getline; + categories = substr($0, 1, 1) != ">" ? "" : substr($0, 3); + getline; # This line should be empty + getline; # First line of description + desc = $0; + next; +} + +/^BEGIN:(VJOURNAL|VTODO)/ { type = $2; print; next } +/^X-ALT-DESC/ && type { next } # drop this alternative description +/^ / && type { next } # drop this folded line (the only content with folded lines will be updated) +/^(DUE|SUMMARY|CATEGORIES|DESCRIPTION|LAST-MODIFIED)/ && type { next } # skip for now, we will write updated fields at the end +/^SEQUENCE/ && type { seq = $2; next } # store sequence number and skip +/^END:/ && type == $2 { + seq = seq ? seq + 1 : 1; + print "SEQUENCE:" seq; + print "LAST-MODIFIED:" zulu; + if (due) print "DUE;VALUE=DATE:" due; + print_fold("SUMMARY:", summary, i, s); + print_fold("CATEGORIES:", categories, i, s); + print_fold("DESCRIPTION:", desc, i, s); + type = ""; +} +{ print }