initial commit: readonly
This commit is contained in:
41
src/awk/altertodo.awk
Normal file
41
src/awk/altertodo.awk
Normal 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
35
src/awk/attach.awk
Normal 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
8
src/awk/attachdd.awk
Normal 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
41
src/awk/attachls.awk
Normal 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
13
src/awk/attachrm.awk
Normal 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
49
src/awk/get.awk
Normal 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
85
src/awk/list.awk
Normal 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
69
src/awk/new.awk
Normal 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
341
src/awk/preview.awk
Normal 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
57
src/awk/update.awk
Normal 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
159
src/lib/awk/vcard.awk
Normal 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
184
src/main.sh
Normal 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
181
src/sh/attachment.sh
Normal 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
69
src/sh/awkscripts.sh
Normal 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
19
src/sh/categories.sh
Normal 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
122
src/sh/cli.sh
Normal 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
29
src/sh/cliinternal.sh
Normal 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
68
src/sh/config.sh
Normal 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
53
src/sh/filter.sh
Normal 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
9
src/sh/helper.sh
Normal 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
264
src/sh/icalendar.sh
Normal 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
28
src/sh/theme.sh
Normal 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:-🦻}"
|
Reference in New Issue
Block a user