initial commit: readonly

This commit is contained in:
2025-07-15 13:41:20 +02:00
commit 4ba328934b
26 changed files with 1956 additions and 0 deletions

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

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

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 }

49
src/awk/get.awk Normal file
View File

@@ -0,0 +1,49 @@
# Retrieve content from iCalendar files
#
# Mandatory variable: `field`.
# Name of field to retrieve.
#
# Optional variable: `format`.
# If `format` is set to "csv", then the content is interpreted as
# comma-separated values, and empty values are dropped.
# If `format` is set to "date", then the content is interpreted as
# a date the output is in the form YYYY-MM-DD.
#
# Optional variable: `oneline`.
# If `oneline` is set, then the all newlines will be replaced by white spaces
@include "lib/awk/vcard.awk"
# print content of field `field`
BEGIN { FS = ":"; regex = "^" field; }
BEGINFILE { type = ""; line = ""; }
/^BEGIN:(VJOURNAL|VTODO)/ { type = $2 }
/^END:/ && $2 == type { nextfile }
$0 ~ regex { line = $0; next; }
/^ / && line { line = line substr($0, 2); next; }
/^[^ ]/ && line { nextfile }
ENDFILE {
if (type) {
# Process line
content = getcontent(line)
if (oneline)
content = singleline(content)
switch (format) {
case "csv" :
split(content, a, ",")
res = ""
for (i in a) {
if (a[i])
res = res "," a[i]
}
print substr(res, 2)
break
case "date" :
if (content)
print substr(parse_dt("", content), 1, 10)
break
default :
print content
break
}
}
}

85
src/awk/list.awk Normal file
View File

@@ -0,0 +1,85 @@
# awk script to generate summary line for vCard entries
#
# See https://www.rfc-editor.org/rfc/rfc6350
#
# Limitations:
# - Use first FN, NICKNAME
# - Ignore properties of FN and NICKNAME
@include "lib/awk/vcard.awk"
BEGIN {
# We require the following variables to be set using -v
# collection_lables: ;-delimited collection=label strings
# nickname_format: format string when nickname is present (has two placeholders)
# fn_format: format string when nickname is absent (has one placeholders)
# group_label: label for group entries
# org_label: label for organizations
# location_label: label for locations
# individual_label: label for individuals
FS = "[:;]";
OFS = "\t"
# Collections
split(collection_labels, mapping, ";");
for (map in mapping)
{
split(mapping[map], m, "=");
collection2label[m[1]] = m[2];
}
}
BEGINFILE {
# Reset variables
nickname = ""
fn = ""
kind = ""
prop = ""
line = ""
delete c
}
FNR == 1 && /^BEGIN:VCARD/ { next }
FNR == 1 { nextfile } # This is not a vcard file
/^END:VCARD/ { nextfile } # Done, go to next file
/^(FN|NICKNAME)/ && c[$1] { prop = ""; next } # FN and NICKNAME may appear multiple times, take first
/^(FN|NICKNAME|KIND)/ { prop = $1; c[prop] = $0; next }
/^[^ ]/ && prop { prop = ""; next }
/^ / && prop { c[prop] = c[prop] substr($0, 2); next }
ENDFILE {
# Construct path
depth = split(FILENAME, path, "/")
fpath = path[depth-1] "/" path[depth]
# Collection name
collection = path[depth-1]
collection = collection in collection2label ? collection2label[collection] : collection
# Content lines
fn = singleline(unescape(getcontent(c["FN"])))
nicknames = getcontent(c["NICKNAME"])
if (nicknames) {
split(nicknames, a, ",")
for (i in a) {
if (a[i] == fn)
continue
nickname = nickname " / " a[i]
}
nickname = substr(nickname, 4)
}
kind = getcontent(c["KIND"])
switch(kind) {
case "group": kind = group_label; break
case "org": kind = org_label; break
case "location": kind = location_label; break
default: kind = individual_label; # "individual"
}
# Build line to be presented
line = nickname ? nickname_format : fn_format
# If nickname contains the string "<<fn>>", then the behaviour is unexpected
gsub("<<nickname>>", nickname, line)
gsub("<<fn>>", fn, line)
print line,
collection,
kind,
fpath
}

69
src/awk/new.awk Normal file
View File

@@ -0,0 +1,69 @@
@include "lib/awk/vcard.awk"
BEGIN {
FS=":";
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1);
}
desc { desc = desc "\\n" escape($0); next; }
/^::: \|>/ && !start { gsub("\"", ""); start = substr(zulu, 1, 8); next; }
/^::: <\|/ && !due { gsub("\"", ""); due = "D" substr($0, 8); next; }
/^# / && !summary { summary = "S" escape(substr($0, 3)); next; }
/^> / && !categories { categories = "C" escape_but_commas(substr($0, 3)); next; }
!$0 && !el { el = 1; next; }
!el { print "Unrecognized header on line "NR": " $0 > "/dev/stderr"; exit 1; }
{ desc = "D" escape($0); next; }
END {
# Sanitize input
type = due ? "VTODO" : "VJOURNAL"
due = substr(due, 2)
summary = substr(summary, 2)
categories = substr(categories, 2)
desc = substr(desc, 2)
if (categories) {
split(categories, a, ",")
categories = ""
for (i in a)
if (a[i])
categories = categories "," a[i]
categories = substr(categories, 2)
}
if (due) {
# Use command line `date` for parsing
cmd = "date -d \"" due "\" +\"%Y%m%d\"";
suc = cmd | getline due
close(cmd)
if (suc != 1)
exit 1
}
# print ical
print "BEGIN:VCALENDAR";
print "VERSION:2.0";
print "CALSCALE:GREGORIAN";
print "PRODID:-//fzf-vjour//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);
if (categories) print_fold("CATEGORIES:", categories);
if (desc) print_fold("DESCRIPTION:", desc);
print "END:" type;
print "END:VCALENDAR"
}

341
src/awk/preview.awk Normal file
View File

@@ -0,0 +1,341 @@
# Generate preview of vCard file
#
# Limitations:
# - Use first occurnece of FN, NICKNAME, ORG
# - other...
@include "lib/awk/vcard.awk"
# Returns type symbol fork "home" and "work" types.
#
# @input type: type string
# @return: symbol or empty string
function type_symbol(type) {
switch(type) {
case "home": return label_home
case "work": return label_work
default: return ""
}
}
# Returns phone symbol
# Empty string is mapped to the default "voice". Any other string not
# recognized resturns the empty string.
#
# @input type: type string
# @return: nonempty string, defaults to voice symbol
function type_tel_symbol(type, res) {
res = type_symbol(type)
if (res)
return res
switch(type) {
case "text": return label_tel_text
case "fax": return label_tel_fax
case "cell": return label_tel_cell
case "video": return label_tel_video
case "pager": return label_tel_pager
case "textphone": return label_tel_textphone
case "voice": return label_tel
case "": return label_tel
default: return ""
}
}
function preview_group() {
print "GROUP"
}
function preview_org() {
print "ORG"
}
function preview_location() {
print "LOC"
}
function preview_individual() {
# Parse unstructured name
fn = singleline(unescape(getcontent(c["FN"])))
nickname = getcontent(c["NICKNAME"])
split(nickname, nicknamearray, ",")
# Parse structured name
if (c["N"]) {
split(getcontent(c["N"]), a, ";")
# Family name
familyname = a[1]
gsub(",", " ", familyname) # Comma-separated list, just concat with WS
familyname = singleline(unescape(familyname))
# Given name
givenname = a[2]
gsub(",", " ", givenname) # Comma-separated list, just concat with WS
givenname = singleline(unescape(givenname))
# Additional name
additionalname = a[3]
gsub(",", " ", additionalname) # Comma-separated list, just concat with WS
additionalname = singleline(unescape(additionalname))
# Prefix
prefixname = a[4]
gsub(",", " ", prefixname) # Comma-separated list, just concat with WS
prefixname = singleline(unescape(prefixname))
# Suffix
suffixname = a[5]
gsub(",", " ", suffixname) # Comma-separated list, just concat with WS
suffixname = singleline(unescape(suffixname))
# Print
if (prefixname)
line = line " " prefixname
if (givenname)
line = line " " givenname
if (additionalname)
line = line " " additionalname
if (familyname)
line = line " " familyname
if (suffixname)
line = line ? line ", " suffixname : suffixname
line = substr(line, 2)
fullname = line
#if (line && line != fn && line != nickname)
# fullname = line
}
# Parse Tel
tel[0] = 0
delete tel[0]
if (n["TEL"]) {
k = 100
for (i=1; i<= n["TEL"]; i++) {
delete b
tmp = ""
nr = singleline(unescape(getcontent(c["TEL", i])))
type = tolower(getparam(c["TEL", i], "TYPE"))
gsub("\"", "", type)
split(type, b, ",")
for (j in b) {
tmp = tmp type_tel_symbol(b[j])
}
type = tmp
pref = tolower(getparam(c["TEL", i], "PREF"))
if (!pref) {
pref = k
k = k + 1
}
while (pref in tel)
pref = pref + 1
tel[pref] = nr
teltype[pref] = type
}
}
# Parse E-Mail
email[0] = 0
delete email[0]
if (n["EMAIL"]) {
k = 100
for (i=1; i<= n["EMAIL"]; i++) {
delete b
tmp = ""
nr = singleline(unescape(getcontent(c["EMAIL", i])))
type = tolower(getparam(c["EMAIL", i], "TYPE"))
gsub("\"", "", type)
split(type, b, ",")
for (j in b) {
tmp = tmp type_symbol(b[j])
}
type = tmp
pref = tolower(getparam(c["EMAIL", i], "PREF"))
if (!pref) {
pref = k
k = k + 1
}
while (pref in email)
pref = pref + 1
email[pref] = nr
emailtype[pref] = type
}
}
# Parse IMPP
impp[0] = 0
delete impp[0]
if (n["IMPP"]) {
k = 100
for (i=1; i<= n["IMPP"]; i++) {
delete b
tmp = ""
nr = singleline(unescape(getcontent(c["IMPP", i])))
type = tolower(getparam(c["IMPP", i], "TYPE"))
gsub("\"", "", type)
split(type, b, ",")
for (j in b) {
tmp = tmp type_symbol(b[j])
}
type = tmp
pref = tolower(getparam(c["IMPP", i], "PREF"))
if (!pref) {
pref = k
k = k + 1
}
while (pref in impp)
pref = pref + 1
impp[pref] = nr
impptype[pref] = type
}
}
# Parse Address
adr[0] = 0
delete adr[0]
if (n["ADR"]) {
k = 100
for (i=1; i<= n["ADR"]; i++) {
delete b
tmp = ""
content = getcontent(c["ADR", i])
split(content, aa, ";")
pobox = aa[1]
extad = aa[2]
street = aa[3]
locality = aa[4]
region = aa[5]
code = aa[6]
country = aa[7]
if (street)
adrstr = singleline(unescape(street))
if (locality) {
if (adrstr)
adrstr = adrstr "\n"
adrstr = adrstr singleline(unescape(locality))
}
if (region) {
if (!locality && adrstr)
adrstr = adrstr "\n"
adrstr = adrstr singleline(unescape(region))
}
if (code) {
if (!locality && !region && adrstr)
adrstr = adrstr "\n"
adrstr = adrstr singleline(unescape(code))
}
if (country) {
if (adrstr)
adrstr = adrstr "\n"
adrstr = adrstr singleline(unescape(country))
}
type = tolower(getparam(c["ADR", i], "TYPE"))
gsub("\"", "", type)
split(type, b, ",")
for (j in b) {
tmp = tmp type_symbol(b[j])
}
type = tmp
pref = tolower(getparam(c["ADR", i], "PREF"))
if (!pref) {
pref = k
k = k + 1
}
while (pref in adr)
pref = pref + 1
adr[pref] = adrstr
adrtype[pref] = type
}
}
# Parse birthday
bday = getcontent(c["BDAY"])
if (bday) {
bday_date[0] = 0
delete bday_date[0]
date_and_or_time(bday, bday_date)
omityear = getparam(c["BDAY"], "X-APPLE-OMIT-YEAR")
if (omityear == bday_date["year"])
bday_date["year"] = ""
}
# Print structured data
if (fullname)
print "# "fullname
# Phone numbers
if (length(tel) >= 1) {
print "\n## ☎️ Phone numbers"
for (pref in tel) {
style = pref == 1 ? "**" : ""
print "- " style teltype[pref] "\t" tel[pref] style
}
}
# E-Mail addresses
if (length(email) >= 1) {
print "\n## 📧 E-mail addresses"
for (pref in email) {
style = pref == 1 ? "**" : ""
print "- " style emailtype[pref] "\t" email[pref] style
}
}
# IMPP
if (length(impp) >= 1) {
print "\n## 💬 Instant messaging"
for (pref in impp) {
style = pref == 1 ? "**" : ""
print "- " style impptype[pref] "\t" impp[pref] style
}
}
# Address
if (length(adr) >= 1) {
for (pref in adr) {
print "\n## " adrtype[pref] " Postal address"
print adr[pref]
}
}
# Birthday
if (bday_date["month"] && bday_date["day"]) {
datestr = "2000-"bday_date["month"] "-" bday_date["day"]
gsub("\"", "", datestr)
cmd = "date -d \"" datestr "\" +\"%e %B\""
suc = cmd | getline res
close(cmd)
if (suc == 1) {
print "\n### 🎂 Birthday"
if (bday_date["year"])
print res " " bday_date["year"]
else
print res
}
}
# Nicknames
if (nickname) {
print "\n### 🏷️ Additional names"
for (i in nicknamearray)
print "- " nicknamearray[i]
}
}
BEGIN { IGNORECASE=1; FS = "[:;]" }
FNR == 1 && /^BEGIN:VCARD/ { next }
FNR == 1 { exit 1 } # This is not a vcard file
/^END:VCARD/ { exit }
/^(FN|NICKNAME|ORG)/ && c[$1] { prop = ""; next } # FN, NICKNAME, ORG may appear multiple times, take first
/^(FN|NICKNAME|KIND|N|BDAY|ORG)/ { prop = toupper($1); n[prop] = 0; c[prop] = $0; next }
/^(ADR|TEL|EMAIL|IMPP|GEO|TITLE|ROLE|ORG|URL)/ { # These entries may appear multiple times
prop = toupper($1); n[prop] = n[prop] + 1; c[prop, n[prop]] = $0; next }
/^[^ ]/ && prop { prop = ""; next }
/^ / && prop {
if (n[prop])
c[prop, n[prop]] = c[prop, n[prop]] substr($0, 2)
else
c[prop] = c[prop] substr($0, 2)
next
}
END {
kind = getcontent(c["KIND"])
switch(kind) {
case "group": preview_group(); break
case "org": preview_org(); break
case "location": preview_location(); break
default: preview_individual(); break
}
}

57
src/awk/update.awk Normal file
View File

@@ -0,0 +1,57 @@
@include "lib/awk/vcard.awk"
BEGIN {
FS=":";
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1);
}
ENDFILE {
if (NR == FNR) {
due = substr(due, 2)
summary = substr(summary, 2)
categories = substr(categories, 2)
desc = substr(desc, 2)
if (categories) {
split(categories, a, ",")
categories = ""
for (i in a)
if (a[i])
categories = categories "," a[i]
categories = substr(categories, 2)
}
if (due) {
# Use command line `date` for parsing
cmd = "date -d \"" due "\" +\"%Y%m%d\"";
suc = cmd | getline due
close(cmd)
if (suc != 1)
exit 1
}
}
}
NR == FNR && desc { desc = desc "\\n" escape($0); next; }
NR == FNR && /^::: <\|/ && !due { gsub("\"",""); due = "D" substr($0, 8); next; }
NR == FNR && /^# / && !summary { summary = "S" escape(substr($0, 3)); next; }
NR == FNR && /^> / && !categories { categories = "C" escape_but_commas(substr($0, 3)); next; }
NR == FNR && !$0 && !el { el = 1; next; }
NR == FNR && !el { print "Unrecognized header on line "NR": " $0 > "/dev/stderr"; exit 1; }
NR == FNR { desc = "D" escape($0); next; }
due && type == "VJOURNAL" { print "Notes and journal entries do not have due dates." > "/dev/stderr"; exit 1; }
/^BEGIN:(VJOURNAL|VTODO)/ { type = $2; print; next; }
/^ / && drop { next; } # drop this folded line
/^X-ALT-DESC/ && type { drop = 1; next; } # drop this alternative description
/^(DUE|SUMMARY|CATEGORIES|DESCRIPTION|LAST-MODIFIED)/ && type { drop = 1; next; } # skip for now, we will write updated fields at the end
{ drop = 0 } # keep everything else
/^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);
print_fold("CATEGORIES:", categories);
print_fold("DESCRIPTION:", desc);
type = "";
}
{ print }

159
src/lib/awk/vcard.awk Normal file
View File

@@ -0,0 +1,159 @@
# Get date from date-and-or-time value
#
# @input str: date-and-or-time string
# @input arr: array, where arr["year"] arr["month"] arr["day"] will be the
# parsed date
function date_and_or_time(str, arr) {
# Some applications hyphenate the string.
# Year and month portions can be omitted by putting dashes.
if (index(str, "T"))
str = substr(str, 1, index(str, "T") - 1)
switch(length((str))) {
case 8:
arr["year"] = substr(str, 1, 4)
arr["month"] = substr(str, 5, 2)
arr["day"] = substr(str, 7, 2)
return
case 10:
arr["year"] = substr(str, 1, 4)
arr["month"] = substr(str, 6, 2)
arr["day"] = substr(str, 9, 2)
return
case 4:
arr["year"] = substr(str, 1, 4)
arr["month"] = ""
arr["day"] = ""
return
case 7:
arr["year"] = substr(str, 1, 4)
arr["month"] = substr(str, 6, 2)
arr["day"] = ""
return
case 6:
arr["year"] = ""
arr["month"] = substr(str, 3, 2)
arr["day"] = substr(str, 5, 2)
return
case 5:
arr["year"] = ""
arr["month"] = ""
arr["day"] = substr(str, 4, 2)
return
}
arr["year"] = ""
arr["month"] = ""
arr["day"] = ""
return
}
# Make string single-line
#
# @input str: String
# @return: String without newlines
function singleline(str) {
gsub("\\n", " ", str)
return str
}
# Escape string to be used as content in iCalendar files.
#
# @input str: String to escape
# @return: Escaped string
function escape(str)
{
gsub("\\\\", "\\\\", str)
gsub("\\n", "\\n", str)
gsub(";", "\\;", str)
gsub(",", "\\,", str)
return str
}
# Escape string to be used as content in iCalendar files, but don't escape
# commas.
#
# @input str: String to escape
# @return: Escaped string
function escape_but_commas(str)
{
gsub("\\\\", "\\\\", str)
gsub("\\n", "\\n", str)
gsub(";", "\\;", str)
return str
}
# Print property with its content and fold according to the iCalendar
# specification.
#
# @local variables: i, s
# @input nameparam: Property name with optional parameters
# @input content: Escaped content
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
}
}
# Unescape string
#
# @local variables: i, c, c2, res
# @input str: String
# @return: Unescaped string
function unescape(str, i, c, c2, res) {
for(i = 1; i <= length(str); i++) {
c = substr(str, i, 1)
if (c != "\\") {
res = res c
continue
}
i++
c2 = substr(str, i, 1)
if (c2 == "n" || c2 == "N") {
res = res "\n"
continue
}
# Alternatively, c2 is "\\" or "," or ";". In each case, append res with
# c2. If the strings has been escaped correctly, then the character c2
# cannot be anything else. To be fail-safe, simply append res with c2.
res = res c2
}
return res
}
# Get specific parameter of logical line.
# Parameter names are case insentitive.
#
# @input str: logical line
# @input param: Parameter name
# @return: Parameter value, comma separated
function getparam(str, param, a, i, paramname, res) {
i = index(str, ";")
if (!i)
return ""
str = substr(str, i + 1)
str = substr(str, 1, index(str, ":") - 1)
split(str, a, ";")
for (i in a) {
paramname = substr(a[i], 1, index(a[i], "=") - 1)
if (toupper(paramname) == toupper(param))
res = res "," substr(a[i], index(a[i], "=") + 1)
}
res = substr(res, 2)
return res
}
# Isolate content part of an iCalendar line, and unescape.
#
# @input str: String
# @return: Escaped content part
function getcontent(str) {
return substr(str, index(str, ":") + 1)
}

184
src/main.sh Normal file
View File

@@ -0,0 +1,184 @@
#!/bin/sh
set -eu
# Always load functions
# Helper functions
. "sh/helper.sh"
# iCalendar routines
. "sh/icalendar.sh"
# Attachment handling
. "sh/attachment.sh"
# Categories handling
. "sh/categories.sh"
# Load environment variables only when not yet loaded
if [ ! "${SCRIPT_LOADED:-}" ]; then
# Read configuration
. "sh/config.sh"
# Read theme
. "sh/theme.sh"
# Load awk scripts
. "sh/awkscripts.sh"
# Mark as loaded
export SCRIPT_LOADED=1
fi
__lines() {
find "$ROOT" -mindepth 2 -maxdepth 2 -type f -name '*.vcf' -print0 | xargs -0 -P 0 \
awk \
-v collection_labels="$COLLECTION_LABELS" \
-v nickname_format="$FORMAT_NICKNAME" \
-v fn_format="$FORMAT_FN" \
-v group_label="$FLAG_GROUP" \
-v org_label="$FLAG_ORGANIZATION" \
-v location_label="$FLAG_LOCATION" \
-v individual_label="$FLAG_INDIVIDUAL" \
"$AWK_LIST" |
sort -g
}
# Program starts here
if [ "${1:-}" = "--help" ]; then
bn="$(basename "$0")"
shift
echo "Usage: $bn [OPTION] [FILTER]...
[OPTION]
--help Show this help and exit
Git Integration:
--git-init Activate git usage and exit
--git <cmd> Run git command and exit
Interactive Mode:
--new [FILTER..] Create new entry interactively and start
[FILTER..] Start with the specified filter
Non-Interactive Mode:
--list [FILTER..] List entries and exit
--import <file> Import vCard file and exit
--collection <nr> Select collection to which to import vCard
file. The argument <nr> is the ordinal
describing the collection. It defaults to the
starting value of 1.
[FILTER]
You may specify any of these filters. Filters can be negated using the
--no-... versions, e.g., --no-individuals. Multiple filters are applied in
conjuction.
--individuals Show individuals only
--groups Show groups only
--organizations Show organizations only
--locations Show locations only
--filter <query> Specify custom query
Examples:
$bn --git log
$bn --new
$bn --individuals
$bn --no-locations --filter \"Athens\"
$bn --list --locations
$bn --import ./contact.vcf
$bn --import ./boss.vcf --collection 2
"
exit
fi
# Command line arguments: Interal use
. "sh/cliinternal.sh"
# Command line arguments
. "sh/cli.sh"
# Parse command-line filter (if any)
. "sh/filter.sh"
if [ -n "${list_option:-}" ]; then
__lines |
$FZF \
--filter="$query" \
--delimiter="\t" \
--no-sort \
--with-nth='{2} {3} {1}'
exit 0
fi
# Set terminal title
if [ "$SET_TERMINAL_TITLE" = "yes" ]; then
printf '\033]0;%s\007' "$TERMINAL_TITLE"
fi
while true; do
query=$(stripws "$query")
query="${query:+"$query "}"
selection=$(
__lines | $FZF \
--ansi \
--query="$query" \
--delimiter="\t" \
--no-sort \
--no-hscroll \
--reverse \
--with-nth='{2} {3} {1}' \
--print-query \
--accept-nth=4 \
--preview="$0 --preview {4}" \
--expect="ctrl-n,ctrl-alt-d,alt-v,ctrl-a,ctrl-t" \
--bind="ctrl-r:reload($0 --reload)" \
--bind="alt-0:change-query()" \
--bind="alt-1:change-query(${COLLECTION1:-} )" \
--bind="alt-2:change-query(${COLLECTION2:-} )" \
--bind="alt-3:change-query(${COLLECTION3:-} )" \
--bind="alt-4:change-query(${COLLECTION4:-} )" \
--bind="alt-5:change-query(${COLLECTION5:-} )" \
--bind="alt-6:change-query(${COLLECTION6:-} )" \
--bind="alt-7:change-query(${COLLECTION7:-} )" \
--bind="alt-8:change-query(${COLLECTION8:-} )" \
--bind="alt-9:change-query(${COLLECTION9:-} )" \
--bind="alt-i:change-query($FLAG_INDIVIDUAL )" \
--bind="alt-g:change-query($FLAG_GROUP )" \
--bind="alt-o:change-query($FLAG_ORGANIZATION )" \
--bind="alt-l:change-query($FLAG_LOCATION )" \
--bind="alt-w:toggle-preview-wrap" \
--bind="ctrl-d:preview-half-page-down" \
--bind="ctrl-u:preview-half-page-up" \
--bind="ctrl-s:execute($SYNC_CMD; [ -n \"${GIT:-}\" ] && ${GIT:-echo} add -A && ${GIT:-echo} commit -am 'Synchronized'; printf 'Press <enter> to continue.'; read -r tmp)" || #--color='preview-bg:#ecc297,preview-fg:black' \
true
)
# Line 1: query
# Line 2: key ("" for enter)
# Line 3: relative file path
lines=$(echo "$selection" | wc -l)
if [ "$lines" -eq 1 ]; then
exit 0
fi
query=$(echo "$selection" | head -n 1)
key=$(echo "$selection" | head -n 2 | tail -n 1)
fname=$(echo "$selection" | head -n 3 | tail -n 1)
file="$ROOT/$fname"
case "$key" in
"ctrl-n")
__new
;;
"ctrl-alt-d")
__delete "$file"
;;
"alt-v")
$EDITOR "$file"
;;
"ctrl-a")
__attachment_view "$file"
;;
"ctrl-t")
cat="$(__select_category)"
[ -n "$cat" ] && query="'$cat'"
;;
"")
__edit "$file"
;;
esac
done

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

@@ -0,0 +1,181 @@
# Add attachment to iCalendar file
#
# @input $1: Path to iCalendar file
__add_attachment() {
file="$1"
shift
sel=$(
$FZF \
--ansi \
--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 \
--ansi \
--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
#
}

69
src/sh/awkscripts.sh Normal file
View File

@@ -0,0 +1,69 @@
AWK_LIST=$(
cat <<'EOF'
@@include awk/list.awk
EOF
)
export AWK_LIST
AWK_PREVIEW=$(
cat <<'EOF'
@@include awk/preview.awk
EOF
)
export AWK_PREVIEW
AWK_ALTERTODO=$(
cat <<'EOF'
@@include awk/altertodo.awk
EOF
)
export AWK_ALTERTODO
AWK_GET=$(
cat <<'EOF'
@@include awk/get.awk
EOF
)
export AWK_GET
AWK_NEW=$(
cat <<'EOF'
@@include awk/new.awk
EOF
)
export AWK_NEW
AWK_UPDATE=$(
cat <<'EOF'
@@include awk/update.awk
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

19
src/sh/categories.sh Normal file
View File

@@ -0,0 +1,19 @@
# List all categories and lest user select
__select_category() {
find "$ROOT" -type f -name "*.ics" -print0 |
xargs -0 -P 0 \
awk -v field="CATEGORIES" -v format="csv" "$AWK_GET" |
tr ',' '\n' |
sort |
uniq |
grep '.' |
$FZF \
--ansi \
--prompt="Select category> " \
--no-sort \
--tac \
--margin="30%,30%" \
--border=bold \
--border-label="Categories" ||
true
}

122
src/sh/cli.sh Normal file
View File

@@ -0,0 +1,122 @@
case "${1:-}" in
"--git-init")
shift
if [ -n "${GIT:-}" ]; then
err "Git already enabled"
return 1
fi
if ! command -v "git" >/dev/null; then
err "Git not installed"
return 1
fi
git -C "$ROOT" init
git -C "$ROOT" add -A
git -C "$ROOT" commit -m 'Initial commit: Start git tracking'
exit
;;
"--git")
shift
if [ -z "${GIT:-}" ]; then
err "Git not supported, run \`$0 --git-init\` first"
return 1
fi
$GIT "$@"
exit
;;
"--new")
shift
__new
export next_filter=1
;;
"--list")
shift
export next_filter=1
export list_option=1
;;
esac
if [ -z "${next_filter:-}" ]; then
# else [FILTER] are the next options
# Here, we have --add-xyz with --collection or nothign
case "${1:-}" in
"--add-note" | "--add-task" | "--add-jour" | "--collection")
noninteractive=1
;;
esac
if [ -n "${noninteractive:-}" ]; then
while [ -n "${1:-}" ]; do
case "$1" in
"--add-note" | "--add-task" | "--add-jour")
if [ -n "${add_option:-}" ]; then
err "What do you want to add?"
exit 1
fi
add_option="$1"
shift
summary=${1-}
if [ -z "$summary" ]; then
err "You did not give a summary"
exit 1
fi
shift
if [ "$add_option" = "--add-task" ] && [ -n "${1:-}" ]; then
case "$1" in
"--"*)
continue
;;
*)
due=$(printf "%s" "$1" | tr -dc "[:alnum:][:blank:]")
shift
if [ -z "$due" ] || ! date -d "$due" >/dev/null 2>&1; then
err "Invalid due date"
exit 1
fi
;;
esac
fi
;;
"--collection")
shift
collection="$(printf "%s" "$COLLECTION_LABELS" |
cut -d ";" -f "${1:-}" 2>/dev/null |
cut -d "=" -f 1 2>/dev/null)"
if [ -z "$collection" ]; then
err "Invalid collection"
exit 1
fi
shift
;;
*)
err "Unknown non-interactive option: $1"
exit 1
;;
esac
done
fi
fi
if [ -n "${noninteractive:-}" ]; then
if [ -z "${add_option:-}" ]; then
err "Specified collection, but nothing to add"
exit 1
fi
if [ -z "${collection:-}" ]; then
collection="$(
printf "%s" "$COLLECTION_LABELS" |
cut -d ";" -f 1 |
cut -d "=" -f 1
)"
fi
case "$add_option" in
"--add-note")
__add_note "$collection" "$summary"
;;
"--add-task")
__add_task "$collection" "$summary" "${due:-}"
;;
"--add-jour")
__add_jour "$collection" "$summary"
;;
esac
exit 0
fi

29
src/sh/cliinternal.sh Normal file
View File

@@ -0,0 +1,29 @@
# Command-line interface for internal use only
# Generate preview of file from selection
if [ "${1:-}" = "--preview" ]; then
shift
name="$1"
shift
file="$ROOT/$name"
awk \
-v label_tel_text="$LABEL_TEL_TEXT" \
-v label_tel_fax="$LABEL_TEL_FAX" \
-v label_tel_cell="$LABEL_TEL_CELL" \
-v label_tel_video="$LABEL_TEL_VIDEO" \
-v label_tel_pager="$LABEL_TEL_PAGER" \
-v label_tel_textphone="$LABEL_TEL_TEXTPHONE" \
-v label_tel="$LABEL_TEL" \
-v label_work="$LABEL_WORK" \
-v label_home="$LABEL_HOME" \
"$AWK_PREVIEW" "$file" |
$CAT
exit
fi
# Reload view
if [ "${1:-}" = "--reload" ]; then
shift
__lines
exit
fi

68
src/sh/config.sh Normal file
View File

@@ -0,0 +1,68 @@
CONFIGFILE="${CONFIGFILE:-$HOME/.config/fzf-contact/config}"
export TERMINAL_TITLE="💌 fzf-contact | Address book"
if [ ! -f "$CONFIGFILE" ]; then
err "Configuration '$CONFIGFILE' not found."
exit 1
fi
# shellcheck source=/dev/null
. "$CONFIGFILE"
if [ -z "${ROOT:-}" ] || [ -z "${COLLECTION_LABELS:-}" ]; then
err "Configuration '$CONFIGFILE' is incomplete."
exit 1
fi
if [ ! -d "$ROOT" ]; then
err "Directory '$ROOT' does not exist."
exit 1
fi
SYNC_CMD="${SYNC_CMD:-}"
SET_TERMINAL_TITLE="${SET_TERMINAL_TITLE:-yes}"
export ROOT
export SYNC_CMD
export COLLECTION_LABELS
export SET_TERMINAL_TITLE
for i in $(seq 9); do
collection=$(printf "%s" "$COLLECTION_LABELS" | cut -d ';' -f "$i" | cut -d '=' -f 1)
label=$(printf "%s" "$COLLECTION_LABELS" | cut -d ';' -f "$i" | cut -d '=' -f 2)
if [ -n "$label" ] && [ ! -d "$ROOT/$collection" ]; then
err "Collection directory for '$label' does not exist."
exit 1
fi
if [ -z "$label" ]; then
export COLLECTION_COUNT=$((i - 1))
break
fi
export "COLLECTION$i=$label"
done
# 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=plain --language=md}
CAT=${CAT:-cat}
export CAT
if command -v "git" >/dev/null && [ -d "$ROOT/.git" ]; then
GIT="git -C $ROOT"
export GIT
fi
export OPEN=${OPEN:-open}

53
src/sh/filter.sh Normal file
View File

@@ -0,0 +1,53 @@
# Build query
while [ -n "${1:-}" ]; do
case "${1:-}" in
"--individuals")
shift
cliquery="${cliquery:-} $FLAG_INDIVIDUAL"
;;
"--no-individuals")
shift
cliquery="${cliquery:-} !$FLAG_INDIVIDUAL"
;;
"--groups")
shift
cliquery="${cliquery:-} $FLAG_GROUP"
;;
"--no-groups")
shift
cliquery="${cliquery:-} !$FLAG_GROUP"
;;
"--organizations")
shift
cliquery="${cliquery:-} $FLAG_ORGANIZATION"
;;
"--no-organizations")
shift
cliquery="${cliquery:-} !$FLAG_ORGANIZATION"
;;
"--locations")
shift
cliquery="${cliquery:-} $FLAG_LOCATION"
;;
"--no-locations")
shift
cliquery="${cliquery:-} !$FLAG_LOCATION"
;;
"--filter")
shift
cliquery="${cliquery:-} $1"
shift
;;
"--no-filter")
shift
cliquery="${cliquery:-} !$1"
shift
;;
*)
err "Unknown option \"$1\""
exit 1
;;
esac
done
query=${cliquery:-}
export query

9
src/sh/helper.sh Normal file
View File

@@ -0,0 +1,9 @@
# Print error message
err() {
echo "$1" >/dev/tty
}
# Strip whitespaces from argument
stripws() {
echo "$@" | sed "s/^ *//" | sed "s/ *$//"
}

264
src/sh/icalendar.sh Normal file
View File

@@ -0,0 +1,264 @@
# Interface to modify iCalendar files
# Wrapper to add entry from markdown file
#
# @input $1: path to markdown file
# @input $2: collection to add to
__add_from_md() {
tmpmd="$1"
shift
collection="$1"
shift
file=""
while [ -f "$file" ] || [ -z "$file" ]; do
uuid=$($UUIDGEN)
file="$ROOT/$collection/$uuid.ics"
done
tmpfile="$tmpmd.ics"
if awk -v uid="$uuid" "$AWK_NEW" "$tmpmd" >"$tmpfile"; then
if [ ! -d "$ROOT/$collection" ]; then
mkdir -p "$ROOT/$collection"
fi
mv "$tmpfile" "$file"
if [ -n "${GIT:-}" ]; then
$GIT add "$file"
$GIT commit -q -m "File added" -- "$file"
fi
else
rm -f "$tmpfile"
err "Failed to create new entry."
fi
rm "$tmpmd"
}
# Noninteractively add note, and fill description from stdin
#
# @input $1: Collection
# @input $2: Summary
__add_note() {
collection="$1"
shift
summary="$1"
shift
tmpmd=$(mktemp --suffix='.md')
{
echo "# $summary"
echo ""
} >"$tmpmd"
if [ ! -t 0 ]; then
cat /dev/stdin >>"$tmpmd"
fi
__add_from_md "$tmpmd" "$collection"
}
# Noninteractively add task, and fill description from stdin
#
# @input $1: Collection
# @input $2: Summary
# @input $3: Due date (optional)
__add_task() {
collection="$1"
shift
summary="$1"
shift
due="${1:-}"
tmpmd=$(mktemp --suffix='.md')
{
echo "::: <| $due"
echo "# $summary"
echo ""
} >"$tmpmd"
if [ ! -t 0 ]; then
cat /dev/stdin >>"$tmpmd"
fi
__add_from_md "$tmpmd" "$collection"
}
# Noninteractively add jounral, and fill description from stdin
#
# @input $1: Collection
# @input $2: Summary
__add_jour() {
collection="$1"
shift
summary="$1"
shift
tmpmd=$(mktemp --suffix='.md')
{
echo "::: |> <!-- keep this line to associate the entry to _today_ -->"
echo "# $summary"
echo ""
} >"$tmpmd"
if [ ! -t 0 ]; then
cat /dev/stdin >>"$tmpmd"
fi
__add_from_md "$tmpmd" "$collection"
}
# Toggle completed status of VTODO
#
# @input $1: Relative path to iCalendar file
__toggle_completed() {
fname="$1"
shift
file="$ROOT/$fname"
tmpfile=$(mktemp)
awk "$AWK_ALTERTODO" "$file" >"$tmpfile"
mv "$tmpfile" "$file"
if [ -n "${GIT:-}" ]; then
$GIT add "$file"
$GIT commit -q -m "Completed toggle" -- "$file"
fi
}
# Change priority of VTODO entry
#
# @input $1: Delta, can be any integer
# @input $2: Relative path to iCalendar file
__change_priority() {
delta=$1
shift
fname="$1"
shift
file="$ROOT/$fname"
tmpfile=$(mktemp)
awk -v delta="$delta" "$AWK_ALTERTODO" "$file" >"$tmpfile"
mv "$tmpfile" "$file"
if [ -n "${GIT:-}" ]; then
$GIT add "$file"
$GIT commit -q -m "Priority changed by $delta" -- "$file"
fi
}
# Edit file
#
# @input $1: File path
__edit() {
file="$1"
shift
tmpmd=$(mktemp --suffix='.md')
due=$(awk -v field="DUE" -v format="date" "$AWK_GET" "$file")
if [ -n "$due" ]; then
echo "::: <| $due" >"$tmpmd"
fi
{
echo "# $(awk -v field="SUMMARY" -v oneline=1 "$AWK_GET" "$file")"
echo "> $(awk -v field="CATEGORIES" -v format="csv" -v oneline=1 "$AWK_GET" "$file")"
echo ""
awk -v field="DESCRIPTION" "$AWK_GET" "$file"
} >>"$tmpmd"
checksum=$(cksum "$tmpmd")
# Open in editor
$EDITOR "$tmpmd" >/dev/tty
# Update only if changes are detected
while [ "$checksum" != "$(cksum "$tmpmd")" ]; do
tmpfile="$tmpmd.ics"
if awk "$AWK_UPDATE" "$tmpmd" "$file" >"$tmpfile"; then
mv "$tmpfile" "$file"
if [ -n "${GIT:-}" ]; then
$GIT add "$file"
$GIT commit -q -m "File modified" -- "$file"
fi
break
else
rm -f "$tmpfile"
err "Failed to update entry. Press <enter> to continue."
read -r tmp
# Re-open in editor
$EDITOR "$tmpmd" >/dev/tty
fi
done
rm "$tmpmd"
}
# Delete file
#
# @input $1: File path
__delete() {
file="$1"
shift
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$file")
while true; do
printf "Do you want to delete the entry with the title \"%s\"? (yes/no): " "$summary" >/dev/tty
read -r yn
case $yn in
"yes")
rm -v "$file"
if [ -n "${GIT:-}" ]; then
$GIT add "$file"
$GIT commit -q -m "File deleted" -- "$file"
fi
break
;;
"no")
break
;;
*)
echo "Please answer \"yes\" or \"no\"." >/dev/tty
;;
esac
done
}
# Add file
__new() {
collection=$(printf "%s" "$COLLECTION_LABELS" |
tr ';' '\n' |
$FZF \
--ansi \
--prompt="Choose collection> " \
--select-1 \
--no-sort \
--tac \
--margin="30%,30%" \
--delimiter='=' \
--border=bold \
--border-label="Collections" \
--with-nth=2 \
--accept-nth=1 || true)
if [ -z "$collection" ]; then
return
fi
file=""
while [ -f "$file" ] || [ -z "$file" ]; do
uuid=$($UUIDGEN)
file="$ROOT/$collection/$uuid.ics"
done
tmpmd=$(mktemp --suffix='.md')
{
echo "::: |> <!-- keep this line to associate the entry to _today_ -->"
echo "::: <| <!-- specify the due date for to-dos, can be empty, a date string, or even \"next Sunday\" -->"
echo "# <!-- write summary here -->"
echo "> <!-- comma-separated list of categories -->"
echo ""
} >"$tmpmd"
checksum=$(cksum "$tmpmd")
# Open in editor
$EDITOR "$tmpmd" >/dev/tty
# Update if changes are detected
while [ "$checksum" != "$(cksum "$tmpmd")" ]; do
tmpfile="$tmpmd.ics"
if awk -v uid="$uuid" "$AWK_NEW" "$tmpmd" >"$tmpfile"; then
if [ ! -d "$ROOT/$collection" ]; then
mkdir -p "$ROOT/$collection"
fi
mv "$tmpfile" "$file"
if [ -n "${GIT:-}" ]; then
$GIT add "$file"
$GIT commit -q -m "File added" -- "$file"
fi
break
else
rm -f "$tmpfile"
err "Failed to create new entry. Press <enter> to continue."
read -r tmp
# Re-open in editor
$EDITOR "$tmpmd" >/dev/tty
fi
done
rm "$tmpmd"
}

28
src/sh/theme.sh Normal file
View File

@@ -0,0 +1,28 @@
# Colors
GREEN="\033[1;32m"
RED="\033[1;31m"
WHITE="\033[1;97m"
CYAN="\033[1;36m"
FAINT="\033[2m"
OFF="\033[m"
# Flags
export FLAG_GROUP="${FLAG_GROUP:-👥}"
export FLAG_ORGANIZATION="${FLAG_ORGANIZATION:-🏛️}"
export FLAG_LOCATION="${FLAG_LOCATION:-🗺️}"
export FLAG_INDIVIDUAL="${FLAG_INDIVIDUAL:-👤}"
# Style
export FORMAT_NICKNAME="${FORMAT_NICKNAME:-${GREEN}<<nickname>>${OFF} ${WHITE}${FAINT}(<<fn>>)${OFF}}"
export FORMAT_FN="${FORMAT_FN:-${GREEN}<<fn>>${OFF}}"
# Preview labels
export LABEL_HOME="${LABEL_HOME:-🏠}"
export LABEL_WORK="${LABEL_WORK:-🏢}"
export LABEL_TEL="${LABEL_TEL:-📞}"
export LABEL_TEL_TEXT="${LABEL_TEL_TEXT:-💬}"
export LABEL_TEL_FAX="${LABEL_TEL_FAX:-📠}"
export LABEL_TEL_CELL="${LABEL_TEL_CELL:-📱}"
export LABEL_TEL_VIDEO="${LABEL_TEL_VIDEO:-📹}"
export LABEL_TEL_PAGER="${LABEL_TEL_PAGER:-📟}"
export LABEL_TEL_TEXTPHONE="${LABEL_TEL_TEXTPHONE:-🦻}"