Compare commits

...

42 Commits

Author SHA1 Message Date
5a3668d6a9 improvement: externalized files 2025-06-18 13:49:56 +02:00
8970e89cc0 improvement: add margin to week view 2025-06-17 16:05:13 +02:00
387688caca bugfix: git message 2025-06-17 15:43:51 +02:00
a9201a7060 bugfix: removed introduced bug with | -> : replacement 2025-06-17 15:41:40 +02:00
760c33b225 bugfix: color repr 2025-06-17 15:39:13 +02:00
2e96e31a5b improved utf-8 usage, and color scheme 2025-06-17 15:19:21 +02:00
6d1d5ce1c6 bugfix: remove description 2025-06-17 15:11:49 +02:00
648ff6c016 bugfix: escape, again 2025-06-17 14:56:10 +02:00
1d92534ffd improvement: attachment key 2025-06-17 12:40:57 +02:00
81c1f94daf bugfix: escaping and iCalendar updates 2025-06-17 12:40:14 +02:00
4d0148e2a3 improvement: adding attachments 2025-06-17 11:25:55 +02:00
fef86eef7a bugfix: open attachment 2025-06-17 11:14:18 +02:00
871a000cbd improvement: attachment feature 2025-06-17 10:59:39 +02:00
c39c45a23a feat: Basic attachment support 2025-06-17 08:38:15 +02:00
cb84445159 bugfix: using date in RFC 5322 format increases stability 2025-06-16 21:31:07 +02:00
1093bc15e5 feat: STATUS support 2025-06-16 15:07:58 +02:00
9e2e3bc35b new colors and informative git commits 2025-06-16 14:15:05 +02:00
caec86c5a0 bugfix: esc 2025-06-16 13:47:04 +02:00
83beaa3ad5 cleaned awk scripts, str escape bugfix, proper use of local variables 2025-06-16 11:04:32 +02:00
ee02a7647b improvement: add q action 2025-06-15 21:57:03 +02:00
bcbd2a9077 feat: non-interactive imports 2025-06-15 21:42:45 +02:00
0b8066923b improvement: Calendar preview: weeks start on Mondays 2025-06-15 21:07:15 +02:00
06020740cc improvements: presentation
- collection label before time
- UTF8 arrow insead of `->`
2025-06-14 23:11:55 +02:00
903c870dba improvement: rearranged keys 2025-06-14 22:30:21 +02:00
4a17512819 bugfix: status was from vjournal not vevent 2025-06-13 23:07:31 +02:00
16193b5554 bug fix: recognize all-day events, improvment: view source with v 2025-06-13 22:39:23 +02:00
e09c38ee29 ctrl-g in dayview 2025-06-13 15:44:11 +02:00
7ed2df2399 feat: git support 2025-06-13 15:31:59 +02:00
23cbe26d94 improvement: week view with ctrl-h and ctrl-l 2025-06-13 14:58:49 +02:00
13aebae71f bug fix: return from dayview 2025-06-13 14:57:17 +02:00
93317350f1 bugfix: cleanup at tend 2025-06-13 14:51:56 +02:00
e67fcca02c feat: import 2025-06-13 14:47:01 +02:00
735665bb92 extended description of configuration 2025-06-13 14:12:29 +02:00
0ffa57373a feat:change tz from within application 2025-06-13 14:07:44 +02:00
6d8520a016 non-recursive, clean 2025-06-13 13:04:35 +02:00
ff898c84c8 feat:location support 2025-06-12 21:47:30 +02:00
dc88d5a965 feat: dayview: jump to next/previous day 2025-06-12 15:22:27 +02:00
cdc008e361 bugfix: corrected week calculation (iso) 2025-06-12 14:57:36 +02:00
7549acb20c bugfix: handle wrong date input in entry creation 2025-06-12 12:44:55 +02:00
aee1a1bf24 improvement: no jump to "day" line in search view 2025-06-12 11:40:47 +02:00
4ebcbe36e3 bugfix: disable up/down limits in search view 2025-06-12 11:39:14 +02:00
acc231027b update readme 2025-06-11 22:27:53 +02:00
33 changed files with 2079 additions and 1016 deletions

View File

@@ -1,4 +1,7 @@
A [fzf](https://github.com/junegunn/fzf)-based **calendar** application with CalDav support.
If you are interested in this, then you may also be interested in the
corresponding journaling application
[fzf-vjour](https://github.com/baumea/fzf-vjour).
Description and Use Case
------------------------
@@ -60,37 +63,81 @@ item_types = ["VEVENT"]
...
```
Here is the complete list of configuration options:
```
### ROOT: Directory containing the collections
### COLLECTION_LABELS: Mappings between collections and labels
### SYNC_CMD (optional): Synchronization command
### DAY_START (optional): Hour of start of the day (defaults to 8)
### DAY_END (optional): Hour of end of the day (defaults to 18)
### EDITOR (optional): Your favorite editor, is usually already exported
### TZ (optional): Your favorite timezone, usually system's choice
### LC_TIME (optional): Your favorite locale for date and time
### ZI_DIR (optional): Location of tzdata, defaults to /usr/share/zoneinfo
```
Usage
-----
Use the default `fzf` keys to navigate your calendar entries, e.g., `ctrl-j`
and `ctrl-k` for going down/up in the list.
After starting `fzf-vcal`, you are presented with a view on the current week.
You can navigate that week using `j` and `h` for going down and up.
Hit `<enter>` on any day, and you will see all entries for that date, including
previews. In both, the week and day views, you can add entries by hitting
`ctrl-n`.
Here is the list of available keybindings:
| Key | View | Action |
| --- | ---- | ------ |
| `enter` | week view | Switch to day view |
| `ctrl-n` | week view | Make a new entry |
| any letter | week view | Search in the list of all entries |
| `backspace` on empty query | week view | Undo search |
| `ctrl-u` | week view | Go back one week |
| `ctrl-d` | week view | Go forth one week |
| `ctrl-alt-u` | week view | Go back one month |
| `ctrl-alt-d` | week view | Go forth one month |
| `ctrl-s` | week view | Run the synchronization command |
| `ctrl-l` | week view | Go to current week |
| `ctrl-g` | week view | Goto date |
| `enter` | day view | Open selected calendar entry in your favorite `$EDITOR` |
| `ctrl-n` | day view | Make a new entry |
| `esc`, `backspace` or `q` | day view | Go back to week view |
| `ctrl-s` | day view | Run the synchronization command |
| `ctrl-alt-d` | day view | Delete selected entry |
| `j` | day view | Scroll down in preview window |
| `k` | day view | Scroll up in preview window |
| `w` | day view | Toggle line wrap in preview window ||
Here is the list of all available keybindings:
### Week view
| Key | Action |
| --- | ------ |
| `q` | quit |
| `enter` | open day |
| `j` | down |
| `k` | up |
| `l` | go to next week |
| `h` | go to previous week |
| `ctrl-l` | go to next month |
| `ctrl-h` | go to previous month |
| `alt-l` | go to next year |
| `alt-h` | go to previous year |
| `ctrl-r` | reload and go to week that contains `today` |
| `ctrl-g` | interactively go to specified week |
| `ctrl-t` | set timezon |
| `ctrl-s` | synchronize |
| `ctrl-n` | add new entry |
| `\` | search all appointment s |
| `x` | Cancel and confirm entry |
| `c` | Unconfirm and confirm entry |
### Day view
| Key | Action |
| --- | ------ |
| `enter` | edit appointment |
| `a` | open attachment list of appointment |
| `j` | down |
| `k` | up |
| `l` | go to next day |
| `h` | go to previous day |
| `ctrl-l` | go to next week |
| `ctrl-h` | go to previous week |
| `alt-l` | go to next month |
| `alt-h` | go to previous month |
| `ctrl-r` | reload and go to `today` |
| `ctrl-g` | interactively go to specified day |
| `ctrl-t` | set timezon |
| `ctrl-s` | synchronize |
| `ctrl-n` | add new entry |
| `ctrl-alt-d` | delete entry |
| `w` | toggle line wrap in preview |
| `ctrl-d` | down in preview |
| `ctrl-u` | up in preview |
| `alt-v` | view raw iCalendar file |
| `esc` | return to week view, you can also do this with `q` or `backspace` |
### There is more
You may also invoke the script with `--help` to see further command-line options.
@@ -98,6 +145,10 @@ Also, you may set `LC_TIME` to your preferred language, and `TZ` to your
preferred timezone. The latter is in particular helpful if you want to take a
look at your calendar relative to being in another timezone.
Git support
-----------
You can track your events with `git` by simply running `fzf-vcal --git-init`.
License
-------
This project is licensed under the [MIT License](./LICENSE).

View File

@@ -5,7 +5,15 @@ GREEN="\033[0;32m"
OFF="\033[m"
NAME="fzf-vcal"
SRC="./src/main.sh"
echo "🐔 ${GREEN}Building${OFF} ${BOLD}$NAME${OFF}"
sed -E 's|@@include (.+)$|cat \1|e' "$SRC" >"$NAME"
tmpdir=$(mktemp -d)
echo "🐔 ${GREEN}Internalize sourced files${OFF}"
sed -E 's|\. "([^$].+)"$|cat src/\1|e' "$SRC" >"$tmpdir/1.sh"
echo "🥚 ${GREEN}Internalize awk scripts${OFF}"
sed -E 's|@@include (.+)$|cat src/\1|e' "$tmpdir/1.sh" >"$tmpdir/2.sh"
echo "🐔 ${GREEN}Internalize awk libraries${OFF}"
sed -E 's|@include "(.+)"$|cat src/\1|e' "$tmpdir/2.sh" >"$NAME"
echo "🥚 ${GREEN}Make executable and cleanup${OFF}"
chmod +x "$NAME"
echo "🥚 ${GREEN}Done${OFF}"
rm -rf "$tmpdir"
echo "🍳 ${GREEN}Done:${OFF} Sucessfully built ${BOLD}${GREEN}$NAME${OFF}"

58
src/awk/approx.awk Normal file
View File

@@ -0,0 +1,58 @@
## src/awk/approx.awk
## Generate single-line approximate information for every iCalendar argument.
##
## @assign collection_labels: See configuration of the current program.
@include "lib/awk/icalendar.awk"
# Functions
# Get relative file path.
#
# @local variables: n, a
# @input path: Path to file
# @return: File path of depth 1
function fn(path, n, a) {
n = split(path, a, "/")
return a[n-1] "/" a[n]
}
# Generate title string that will be displayed to user. Here, the start date
# gets a monthly resolution.
#
# @input start: Parsed content of DTSTART field
# @input summary: Content of SUMMARY field
# @return: colorized single-line title string
function title(start, summary) {
summary = getcontent(summary)
gsub("\n", " ", summary) # This will be put on a single line
gsub("\\|", ":", summary) # we use "|" as delimiter
depth = split(FILENAME, path, "/")
collection = depth > 1 ? path[depth-1] : ""
collection = collection in collection2label ? collection2label[collection] : collection
return FAINT "~ " collection " " gensub(/^[^0-9]*([0-9]{4})([0-9]{2}).*$/, "\\1-\\2", "1", start) " " summary OFF
}
# AWK program
BEGIN {
FS="[:;=]"
OFS="|"
split(collection_labels, mapping, ";")
for (map in mapping)
{
split(mapping[map], m, "=")
collection2label[m[1]] = m[2]
}
# Colors
FAINT = "\033[2m"
OFF = "\033[m"
}
BEGINFILE { inside = 0; rs = 0; dur = 0; summary = ""; start = "ERROR"; end = "ERROR" }
/^END:VEVENT/ { print "~", start, dur ? start " " end : end, title(start, summary), fn(FILENAME); nextfile }
/^DTSTART/ && inside { start = parse() }
/^DTEND/ && inside { end = parse() }
/^DURATION/ && inside { end = parse_duration($NF); dur = 1 }
/^[^ ]/ && rs { rs = 0 }
/^ / && rs { summary = summary substr($0, 2) }
/^SUMMARY/ && inside { rs = 1; summary = $0 }
/^BEGIN:VEVENT/ { inside = 1 }

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

@@ -0,0 +1,35 @@
## src/awk/attach.awk
## Prepend attachment to iCalendar file.
##
## @assign file: Path to base64-encoded content
## @assign mime: Mime
## @assign filename: Original filename
# Functions
# Write attachment
#
# @local variables: line, aline
function write_attachment( line, aline, fl) {
line = "ATTACH;ENCODING=BASE64;VALUE=BINARY;FMTTYPE="mime";FILENAME="filename":"
fl = 1
while (getline aline <file) {
line = line aline
if (fl && length(line) >= 73) {
print substr(line, 1, 73)
line = substr(line, 74)
fl = 0
}
while (length(line) >= 72) {
print " "substr(line, 1, 72)
line = substr(line, 73)
}
}
if (line)
print " "line
}
# AWK program
/^END:VEVENT$/ { write_attachment() }
{ print }

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

@@ -0,0 +1,8 @@
BEGIN { FS="[:;]" }
/^END:VEVENT$/ { 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:VEVENT$/ { 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:VEVENT$/ { ins = 0; exit }
l && !r { att_info(i, l); l = "" }
/^ / && r { l = l substr($0, 2); r = cont_reading($0) }
/^ATTACH/ && ins { i++; l = $0; r = cont_reading($0) }
/^BEGIN:VEVENT$/ { ins = 1 }

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

@@ -0,0 +1,13 @@
## src/awk/attachrm.awk
## Remove attachment from iCalendar file.
##
## @assign id: Attachment number to remove
BEGIN { FS="[:;]" }
/^END:VEVENT$/ { ins = 0 }
/^[^ ]/ && a { a = 0 }
/^ / && a { next }
/^ATTACH/ && ins { i++; }
/^ATTACH/ && ins && i == id { a = 1; next }
/^BEGIN:VEVENT$/ { ins = 1 }
{ print }

View File

@@ -1,3 +1,9 @@
## src/awk/calannot.awk
## Annotate monthly calendar
##
## @assign cur: Day-of-month to mark as `today`
## @assign day: Day-of-month to highlight
BEGIN {
BLACK = "\033[1;30m"
GREEN = "\033[1;32m"

23
src/awk/calshift.awk Normal file
View File

@@ -0,0 +1,23 @@
## src/awk/calshift.awk
## Rearrange days of a monthly output from cal (1), such that Monday is the
## first day of the week.
BEGIN {
ORS = ""
W3 = " "
W17 = W3 W3 W3 W3 W3 " "
}
NR == 1 { i++; print $0 "\n"; next }
NR == 2 { i++; print substr($0, 4, 17) " " substr($0, 1, 3) " \n"; next }
NR == 3 && /^ 1/ { print W17; }
NR == 3 && /^ / { print substr($0, 4, 17); next }
/[0-9]/ {
i++
print " " substr($0, 1, 3) " \n" substr($0, 4, 17)
}
END {
i++
print " " W3 " \n"
for (i; i<8; i++)
print " " W17 W3 " \n"
}

View File

@@ -1,26 +1,86 @@
# 11:00|13:00|1748422800|1748430000|fpath|desc...
# 00:00|00:00|1748296800|1748383200|fpath|desc...
function allday(desc) {
return ITALIC FAINT " (allday) " OFF desc
## src/awk/dayview.awk
## Generate the view of a day from lines of the form
## ```
## <start_date>|<end_date>|<start_time>|<end_time>|<file_path>|<collection>|<description>
## ```.
##
## @assign today: Date of `today` in the format %D (%m/%d/%y)
## @assign daystart: Hour of start of the day
## @assign dayend: Hour of end of the day
# Functions
# Set event color based on status
# @input status: Event status, one of TENTATIVE, CONFIRMED, CANCELLED
# @return: Color modifier
function color_from_status(status) {
return status == "CANCELLED" ? STRIKE CYAN : status == "TENTATIVE" ? FAINT CYAN : CYAN
}
function endstoday(stop, desc) {
return CYAN " -- " stop OFF ": " desc
# Return line for all-day event.
#
# @local variables: color
# @input collection: Collection symbol
# @input desc: Event description
# @input status: Event status, one of TENTATIVE, CONFIRMED, CANCELLED
# @return: Single-line string
function allday(collection, desc, status, color) {
color = color_from_status(status)
return collection " " LIGHT_CYAN ITALIC FAINT " (allday) " OFF color desc OFF
}
function slice(start, stop, desc) {
# Return line for multi-day event, or event that starts at midnight, which ends today.
#
# @local variables: color
# @input stop: Time at which the event ends
# @input collection: Collection symbol
# @input desc: Event description
# @input status: Event status, one of TENTATIVE, CONFIRMED, CANCELLED
# @return: Single-line string
function endstoday(stop, collection, desc, status) {
color = color_from_status(status)
return collection " " LIGHT_CYAN " → " stop ": " OFF color desc OFF
}
# Return line for event that starts sometime today.
#
# @local variables: color
# @input start: Time at which the event starts
# @input stop: Time at which the event ends
# @input collection: Collection symbol
# @input desc: Event description
# @input status: Event status, one of TENTATIVE, CONFIRMED, CANCELLED
# @return: Single-line string
function slice(start, stop, collection, desc, status) {
color = color_from_status(status)
if (stop == "00:00")
return CYAN start " -- " OFF ": " desc
return collection " " LIGHT_CYAN start " " ": " OFF color desc OFF
else
return CYAN start OFF " -- " CYAN stop OFF ": " desc
return collection " " LIGHT_CYAN start " " stop ": " OFF color desc OFF
}
# Print line for a single hour entry.
#
# @input hour: Hour of the entry
function hrline(hour) {
hour = hour < 10 ? "0"hour : hour
print hour, "", "", "", FAINT hour ":00 ----------------------" OFF
print today, hour, "", "", "", " " FAINT hour ":00 ----------------------" OFF
}
# Print lines for hour entries before an event that starts at `start` and stops
# at `stop`.
#
# @local variables: starth, stoph, tmp, i
# @input start: Time at which the event starts
# @input stop: Time at which the event ends
# @input h: Last event-free hour
# @return: Hour of now last event-free hour
function hrlines(start, stop, h, starth, stoph, tmp, i) {
starth = substr(start, 1, 2)
stoph = substr(stop, 1, 2)
tmp = substr(start, 4, 2) == "00" ? 0 : 1
for (i=h; i < starth + tmp; i++)
for (i=h; i < starth + tmp && i < dayend; i++)
hrline(i)
tmp = substr(stop, 4, 2) == "00" ? 0 : 1
if (stoph + tmp < daystart)
@@ -28,23 +88,22 @@ function hrlines(start, stop, h, starth, stoph, tmp, i) {
else
return stoph + tmp
}
# AWK program
BEGIN {
FS = "|"
GREEN = "\033[1;32m"
RED = "\033[1;31m"
WHITE = "\033[1;97m"
LIGHT_CYAN = "\033[1;36m"
CYAN = "\033[1;36m"
ITALIC = "\033[3m"
FAINT = "\033[2m"
STRIKE = "\033[9m"
OFF = "\033[m"
OFS = "|"
}
$1 == "00:00" && $2 == "00:00" { print $1, $3, $4, $5, allday($6); next }
$1 == "00:00" { print $1, $3, $4, $5, endstoday($2, $6); next }
$1 == "00:00" && $2 == "00:00" { print today, $1, $3, $4, $5, allday($6, $7, $8); next }
$1 == "00:00" { print today, $1, $3, $4, $5, endstoday($2, $6, $7, $8); next }
$1 ~ /^[0-9]{2}:[0-9]{2}$/ {
daystart = hrlines($1, $2, daystart, starth, stoph, tmp, i)
print $1, $3, $4, $5, slice($1, $2, $6)
}
END {
hrlines(dayend":00", 0, daystart, starth, stoph, tmp, i)
daystart = hrlines($1, $2, daystart)
print today, $1, $3, $4, $5, slice($1, $2, $6, $7, $8)
}
END { hrlines(dayend":00", 0, daystart) }

View File

@@ -1,18 +1,18 @@
# print content of field `field`
BEGIN { FS = ":"; regex = "^" field; }
## src/awk/get.awk
## Print content of a field of an iCalendar file.
##
## @assign field: Field name
@include "lib/awk/icalendar.awk"
BEGIN { FS = ":"; regex = "^" field }
/^BEGIN:VEVENT$/ { inside = 1 }
/^END:VEVENT$/ { exit }
$0 ~ regex { content = $0; next; }
/^ / && content { content = content substr($0, 2); next; }
$0 ~ regex { content = $0; next }
/^ / && content { content = content substr($0, 2); next }
/^[^ ]/ && content { exit }
END {
if (!inside) { exit }
# Process content line
content = substr(content, index(content, ":") + 1);
gsub("\\\\n", "\n", content);
gsub("\\\\N", "\n", content);
gsub("\\\\,", ",", content);
gsub("\\\\;", ";", content);
gsub("\\\\\\\\", "\\", content);
print content;
print getcontent(content)
}

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

@@ -0,0 +1,10 @@
## src/awk/has.awk
## Decide if VEVENT file has a specific field.
##
## @assign field: Field name
# AWK program
BEGIN { FS = "[:;]" }
/^BEGIN:VEVENT$/ { ins = 1 }
/^END:VEVENT$/ { exit 1 }
ins && $1 == field { exit 0 }

View File

@@ -1,79 +0,0 @@
function parse( dt) {
# Get timezone information
dt = "";
for (i=2; i<NF-1; i+=2) {
if ($i == "TZID") {
dt = "TZ=\"" $(i+1) "\" ";
break;
}
}
# Get date/datetime
return length($NF) == 8 ?
dt $NF :
dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", $NF);
}
function parse_duration( dt, dta, i, n, a, seps) {
n = split($NF, a, /[PTWHMSD]/, seps);
delete dta;
for (i=2; i<=n; i++) {
if(seps[i] == "W") dta["weeks"] = a[i];
if(seps[i] == "H") dta["hours"] = a[i];
if(seps[i] == "M") dta["minutes"] = a[i];
if(seps[i] == "S") dta["seconds"] = a[i];
if(seps[i] == "D") dta["days"] = a[i];
}
dt = a[1] ? a[1] : "+";
for (i in dta) {
dt = dt " " dta[i] " " i;
}
return dt;
}
function fn(path, n, a) {
n = split(path, a, "/");
return a[n-1] "/" a[n];
}
function title(start, summary) {
summary = substr(summary, index(summary, ":") + 1);
#gsub("\\\\n", "\n", summary); # one-liner
#gsub("\\\\N", "\n", summary); # one-liner
gsub("\\\\n", " ", summary);
gsub("\\\\N", " ", summary);
gsub("\\\\,", ",", summary);
gsub("\\\\;", ";", summary);
gsub("\\\\\\\\", "\\", summary);
gsub("\\|", ":", summary); # we use "|" as delimiter
depth = split(FILENAME, path, "/");
collection = depth > 1 ? path[depth-1] : "";
collection = collection in collection2label ? collection2label[collection] : collection;
return FAINT "~ " collection " " gensub(/^[^0-9]*([0-9]{4})([0-9]{2}).*$/, "\\1-\\2", "1", start) " " summary OFF
}
BEGIN {
FS="[:;=]";
OFS="|"
split(collection_labels, mapping, ";");
for (map in mapping)
{
split(mapping[map], m, "=");
collection2label[m[1]] = m[2];
}
# Colors
GREEN = "\033[1;32m";
RED = "\033[1;31m";
WHITE = "\033[1;97m";
CYAN = "\033[1;36m";
FAINT = "\033[2m";
OFF = "\033[m";
}
BEGINFILE { inside = 0; rs = 0; dur = 0; summary = ""; start = "ERROR"; end = "ERROR" }
/^END:VEVENT/ { print "~", start, dur ? start " " end : end, title(start, summary), fn(FILENAME, n, a); nextfile }
/^DTSTART/ && inside { start = parse( dt) }
/^DTEND/ && inside { end = parse( dt) }
/^DURATION/ && inside { end = parse_duration( dt, dta, i, n, a, seps); dur = 1 }
/^[^ ]/ && rs { rs = 0 }
/^ / && rs { summary = summary substr($0, 2); }
/^SUMMARY/ && inside { rs = 1; summary = $0; }
/^BEGIN:VEVENT/ { inside = 1 }

View File

@@ -1,17 +1,33 @@
BEGIN { FS="|"; i=0; dlt = -259200; spw = 604800; }
## src/awk/merge.awk
## Merge a file that contains pairs of lines for start and end dates of events
## with the approximate data file, and group the iCalendar file paths according
## to the weeks at which the events take place.
# AWK program
BEGIN { FS="|" }
NR == FNR {
i = i + 1;
from[i] = int(($1 + dlt)/ spw);
getline;
to[i] = int(($1 + dlt) / spw);
i = i + 1
from_year[i] = $1
from_week[i] = $2
getline
to_year[i] = $1
to_week[i] = $2
next
} # Load start and end week numbers from first file
{
if (from[FNR] > to[FNR])
print "FNR", FNR, ":", from[FNR],"-",to[FNR], " ",$0;
for(i=from[FNR]; i<=to[FNR]; i++) {
week[i] = week[i] " " $5
year_i = from_year[FNR]
week_i = from_week[FNR]
year_end = to_year[FNR]
week_end = to_week[FNR]
while(year_i <= year_end && (year_i < year_end || week_i <= week_end)) {
label = year_i"|"week_i
week[label] = week[label] " " $5
week_i++
if (week_i > 53) {
week_i = 1
year_i++
}
}
}
END { for (i in week) print i week[i]; }
END { for (label in week) print label week[label] }

View File

@@ -1,95 +1,105 @@
function escape(str)
{
gsub("\\\\", "\\\\", str);
gsub(";", "\\\\;", str);
gsub(",", "\\\\,", str);
}
## src/awk/new.awk
## Generate iCalendar file from markdown description.
##
## @assign uid: UID to use
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;
}
}
@include "lib/awk/icalendar.awk"
# AWK program
BEGIN {
FS=":";
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1);
FS=":"
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1)
}
desc { desc = desc "\\n" $0; next; }
readdesc { desc = desc ? desc "\\n" escape($0) : escape($0); next }
{
from = substr($0, 1, 6) == "::: |>" ? substr($0, 8) : "";
from = substr($0, 1, 6) == "::: |>" ? substr($0, 8) : ""
if (!from)
exit 1
getline
to = substr($0, 1, 6) == "::: <|" ? substr($0, 8) : "";
to = substr($0, 1, 6) == "::: <|" ? substr($0, 8) : ""
if (!to)
exit 1
getline
summary = substr($0, 1, 2) == "# " ? substr($0, 3) : ""
location = substr($0, 1, 2) == "@ " ? escape(substr($0, 3)) : ""
if (location) getline
summary = substr($0, 1, 2) == "# " ? escape(substr($0, 3)) : ""
if (!summary)
exit 1
getline # This line should be empty
getline # First line of description
desc = $0;
next;
if ($0 != "")
exit 1
readdesc = 1
next
}
END {
# Sanitize input
# If nanoseconds are not 0, then we assume user enterd "tomorrow" or
# If nanoseconds are not 0, then we assume user entered "tomorrow" or
# something the like, and we make this a date entry, as opposed to a
# date-time entry.
from = from ? from : "now"
cmd = "date -d \"" from "\" +\"%N\"";
# Similarly, if the time is 00:00, we make this a date, as opposed to a
# date-time entry.
gsub("\"", "\\\"", from)
cmd = "date -d \"" from "\" +\"%N\""
cmd | getline n
close(cmd)
n = n + 0
cmd = "date -d \"" from "\" +\"%H%M\""
cmd | getline t
close(cmd)
t = t + 0
if (t == 0) {
if (n != 0 || t == 0) {
from_type = "DATE"
cmd = "date -d \"" from "\" +\"%Y%m%d\""
} else {
from_type = "DATE-TIME"
cmd = "date -d \"" from "\" +\"@%s\" | xargs date -u +\"%Y%m%dT%H%M00Z\" -d"
} else {
from_type = "DATE"
cmd = "date -d \"" from "\" +\"%Y%m%d\"";
}
cmd | getline from
suc = cmd | getline from
close(cmd)
if (suc != 1) {
exit 1
}
#
to = to ? to : "now"
cmd = "date -d \"" to "\" +\"%N\"";
gsub("\"", "\\\"", to)
cmd = "date -d \"" to "\" +\"%N\""
cmd | getline n
close(cmd)
n = n + 0
cmd = "date -d \"" to "\" +\"%H%M\""
cmd | getline t
close(cmd)
t = t + 0
if (t == 0) {
if (n != 0 || t == 0) {
to_type = "DATE"
cmd = "date -d \"" to "\" +\"%Y%m%d\""
} else {
to_type = "DATE-TIME"
cmd = "date -d \"" to "\" +\"@%s\" | xargs date -u +\"%Y%m%dT%H%M00Z\" -d"
} else {
to_type = "DATE"
cmd = "date -d \"" to "\" +\"%Y%m%d\"";
}
cmd | getline to
suc = cmd | getline to
close(cmd)
escape(summary);
escape(desc);
if (suc != 1) {
exit 1
}
# print ical
print "BEGIN:VCALENDAR";
print "VERSION:2.0";
print "CALSCALE:GREGORIAN";
print "PRODID:-//fab//awk//EN";
print "BEGIN:VCALENDAR"
print "VERSION:2.0"
print "CALSCALE:GREGORIAN"
print "PRODID:-//fab//awk//EN"
print "BEGIN:VEVENT"
print "DTSTAMP:" zulu;
print "UID:" uid;
print "CLASS:PRIVATE";
print "CREATED:" zulu;
print "SEQUENCE:1";
print "LAST-MODIFIED:" zulu;
print "STATUS:FINAL";
print "DTSTAMP:" zulu
print "UID:" uid
print "CLASS:PRIVATE"
print "CREATED:" zulu
print "SEQUENCE:1"
print "LAST-MODIFIED:" zulu
print "STATUS:CONFIRMED"
print "DTSTART;VALUE=" from_type ":" from
print "DTEND;VALUE=" to_type ":" to
if (summary) print_fold("SUMMARY:", summary, i, s);
if (desc) print_fold("DESCRIPTION:", desc, i, s);
if (summary) print_fold("SUMMARY:", summary)
if (desc) print_fold("DESCRIPTION:", desc)
if (location) print_fold("LOCATION:", location)
print "END:VEVENT"
print "END:VCALENDAR"
}

View File

@@ -1,46 +1,27 @@
function parse( dt) {
# Get timezone information
dt = "";
for (i=2; i<NF-1; i+=2) {
if ($i == "TZID") {
dt = "TZ=\"" $(i+1) "\" ";
break;
}
}
# Get date/datetime
return length($NF) == 8 ?
dt $NF :
dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", $NF);
}
## src/awk/parse.awk
## Parse iCalendar file and print its key aspects:
## ```
## <start> <end> <fpath> <collection> <status> <summary>
## ```.
##
## @assign collection_labels: See configuration of the current program.
function parse_duration( dt, dta, i, n, a, seps) {
n = split($NF, a, /[PTWHMSD]/, seps);
delete dta;
for (i=2; i<=n; i++) {
if(seps[i] == "W") dta["weeks"] = a[i];
if(seps[i] == "H") dta["hours"] = a[i];
if(seps[i] == "M") dta["minutes"] = a[i];
if(seps[i] == "S") dta["seconds"] = a[i];
if(seps[i] == "D") dta["days"] = a[i];
}
dt = a[1] ? a[1] : "+";
for (i in dta) {
dt = dt " " dta[i] " " i;
}
return dt;
}
@include "lib/awk/icalendar.awk"
function print_data(start, dur, end, summary, cmd, collection) {
summary = substr(summary, index(summary, ":") + 1);
gsub("\\\\n", " ", summary); # one-liner
gsub("\\\\N", " ", summary); # one-liner
gsub("\\\\,", ",", summary);
gsub("\\\\;", ";", summary);
gsub("\\\\\\\\", "\\", summary);
depth = split(FILENAME, path, "/");
# Print string of parsed data.
#
# @local variables: cmd, collection, depth, path
# @input start: Start time of event
# @input dur: Boolean that indicates that `end` specifies a duration
# @input end: End time of event, or event duration (see `dur`)
# @input summary: Content of SUMMARY field of the event
function print_data(start, dur, end, summary, cmd, collection, depth, path) {
summary = getcontent(summary)
gsub("\n", " ", summary) # This will be put on a single line
depth = split(FILENAME, path, "/")
fpath = path[depth-1] "/" path[depth]
collection = depth > 1 ? path[depth-1] : "";
collection = collection in collection2label ? collection2label[collection] : collection;
collection = depth > 1 ? path[depth-1] : ""
collection = collection in collection2label ? collection2label[collection] : collection
collection = collection2label[path[depth-1]]
end = dur ? start " " end : end
cmd = "date -d '" start "' +\"%s\""
@@ -49,23 +30,26 @@ function print_data(start, dur, end, summary, cmd, collection) {
cmd = "date -d '" end "' +\"%s\""
cmd | getline end
close(cmd)
print start, end, fpath, collection, summary
status = status ? status : "CONFIRMED"
print start, end, fpath, collection, status, summary
}
# AWK program
BEGIN {
FS="[:;=]";
split(collection_labels, mapping, ";");
FS="[:;=]"
split(collection_labels, mapping, ";")
for (map in mapping)
{
split(mapping[map], m, "=");
collection2label[m[1]] = m[2];
split(mapping[map], m, "=")
collection2label[m[1]] = m[2]
}
}
/^END:VEVENT/ && inside { print_data(start, dur, end, summary, cmd, collection); exit }
/^DTSTART/ && inside { start = parse( dt) }
/^DTEND/ && inside { end = parse( dt) }
/^DURATION/ && inside { end = parse_duration( dt, dta, i, n, a, seps); dur = 1 }
/^END:VEVENT/ && inside { print_data(start, dur, end, summary); exit }
/^DTSTART/ && inside { start = parse() }
/^DTEND/ && inside { end = parse() }
/^DURATION/ && inside { end = parse_duration($NF); dur = 1 }
/^STATUS/ && inside { status = $NF }
/^[^ ]/ && rs { rs = 0 }
/^ / && rs { summary = summary substr($0, 2); }
/^SUMMARY/ && inside { rs = 1; summary = $0; }
/^ / && rs { summary = summary substr($0, 2) }
/^SUMMARY/ && inside { rs = 1; summary = $0 }
/^BEGIN:VEVENT/ { inside = 1 }

27
src/awk/set.awk Normal file
View File

@@ -0,0 +1,27 @@
## src/awk/set.awk
## Set or replace the content of a specified field in the iCalendar file.
##
## @assign field: iCalendar field
## @assign value: Content to set it to
##
## LIMITATION: This program does not fold long content lines.
@include "lib/awk/icalendar.awk"
BEGIN { FS = "[:;]"; zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1) }
/^BEGIN:VEVENT$/ { inside = 1 }
/^END:VEVENT$/ {
inside = 0
if (!duplic)
print field ":" escape(value)
seq = seq ? seq + 1 : 1
print "SEQUENCE:" seq
print "LAST-MODIFIED:" zulu
}
$1 == field && inside { con = 1; duplic = 1; print field ":" escape(value); next }
$1 == field && duplic { con = 1; next }
/^ / && con { next }
/^[^ ]/ && con { con = 0 }
/^SEQUENCE/ && inside { seq = $2; next } # store sequence number and skip
/^LAST-MODIFIED/ && inside { next }
{ print }

View File

@@ -1,101 +1,103 @@
function getcontent(content_line, prop)
{
return substr(content_line[prop], index(content_line[prop], ":") + 1);
}
## src/awk/update.awk
## Update iCalendar file from markdown file.
function escape(str)
{
gsub("\\\\", "\\\\", str);
gsub(";", "\\\\;", str);
gsub(",", "\\\\,", str);
}
function print_fold(nameparam, content, i, s)
{
i = 74 - length(nameparam);
s = substr(content, 1, i);
print nameparam s;
s = substr(content, i+1, 73);
i = i + 73;
while (s)
{
print " " s;
s = substr(content, i+1, 73);
i = i + 73;
}
}
@include "lib/awk/icalendar.awk"
BEGIN {
FS=":";
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1);
FS=":"
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1)
}
ENDFILE {
if (NR == FNR)
{
# If nanoseconds are not 0, then we assume user enterd "tomorrow" or
# If nanoseconds are not 0, then we assume user entered "tomorrow" or
# something the like, and we make this a date entry, as opposed to a
# date-time entry.
from = from ? from : "now"
cmd = "date -d \"" from "\" +\"%N\"";
gsub("\"", "\\\"", from)
cmd = "date -d \"" from "\" +\"%N\""
cmd | getline n
close(cmd)
n = n + 0
cmd = "date -d \"" from "\" +\"%H%M\""
cmd | getline t
close(cmd)
t = t + 0
if (t == 0) {
if (n != 0 || t == 0) {
from_type = "DATE"
cmd = "date -d \"" from "\" +\"%Y%m%d\""
} else {
from_type = "DATE-TIME"
cmd = "date -d \"" from "\" +\"@%s\" | xargs date -u +\"%Y%m%dT%H%M00Z\" -d"
} else {
from_type = "DATE"
cmd = "date -d \"" from "\" +\"%Y%m%d\"";
}
cmd | getline from
suc = cmd | getline from
close(cmd)
if (suc != 1) {
exit 1
}
#
to = to ? to : "now"
cmd = "date -d \"" to "\" +\"%N\"";
gsub("\"", "\\\"", to)
cmd = "date -d \"" to "\" +\"%N\""
cmd | getline n
close(cmd)
n = n + 0
cmd = "date -d \"" to "\" +\"%H%M\""
cmd | getline t
close(cmd)
t = t + 0
if (t == 0) {
if (n != 0 || t == 0) {
to_type = "DATE"
cmd = "date -d \"" to "\" +\"%Y%m%d\""
} else {
to_type = "DATE-TIME"
cmd = "date -d \"" to "\" +\"@%s\" | xargs date -u +\"%Y%m%dT%H%M00Z\" -d"
} else {
to_type = "DATE"
cmd = "date -d \"" to "\" +\"%Y%m%d\"";
}
cmd | getline to
suc = cmd | getline to
close(cmd)
if (suc != 1) {
exit 1
}
}
escape(summary);
escape(desc);
}
NR == FNR && desc { desc = desc "\\n" $0; next; }
NR == FNR && readdesc { desc = desc ? desc "\\n" escape($0) : escape($0); next }
NR == FNR {
from = substr($0, 1, 6) == "::: |>" ? substr($0, 8) : "";
from = substr($0, 1, 6) == "::: |>" ? substr($0, 8) : ""
if (!from)
exit 1
getline
to = substr($0, 1, 6) == "::: <|" ? substr($0, 8) : "";
to = substr($0, 1, 6) == "::: <|" ? substr($0, 8) : ""
if (!to)
exit 1
getline
summary = substr($0, 1, 2) == "# " ? substr($0, 3) : ""
location = substr($0, 1, 2) == "@ " ? escape(substr($0, 3)) : ""
if (location) getline
summary = substr($0, 1, 2) == "# " ? escape(substr($0, 3)) : ""
if (!summary)
exit 1
getline # This line should be empty
getline # First line of description
desc = $0;
next;
if ($0 != "")
exit 1
readdesc = 1
next
}
/^BEGIN:VEVENT$/ { inside = 1; print; next }
/^X-ALT-DESC/ && inside { next } # drop this alternative description
/^ / && inside { next } # drop this folded line (the only content with folded lines will be updated)
/^(DTSTART|DTEND|SUMMARY|CATEGORIES|DESCRIPTION|LAST-MODIFIED)/ && inside { next } # skip for now, we will write updated fields at the end
/^SEQUENCE/ && inside { seq = $2; next } # store sequence number and skip
/^END:VEVENT$/ {
seq = seq ? seq + 1 : 1
print "SEQUENCE:" seq
print "LAST-MODIFIED:" zulu
print "DTSTART;VALUE=" from_type ":" from
print "DTEND;VALUE=" to_type ":" to
print_fold("SUMMARY:", summary, i, s)
print_fold("DESCRIPTION:", desc, i, s)
print_fold("SUMMARY:", summary)
print_fold("DESCRIPTION:", desc)
print_fold("LOCATION:", location)
inside = ""
skipf = 0
}
/^BEGIN:VEVENT$/ { inside = 1 }
/^ / && skipf { next } # drop this folded line
/^[^ ]/ && skipf { skipf = 0 }
/^(DTSTART|DTEND|SUMMARY|LOCATION|CATEGORIES|DESCRIPTION|LAST-MODIFIED)/ && inside { skipf = 1; next } # skip for now, we will write updated fields at the end
/^X-ALT-DESC/ && inside { skipf = 1; next } # skip
/^SEQUENCE/ && inside { seq = $2; next } # store sequence number and skip
{ print }

View File

@@ -1,44 +0,0 @@
function parse( dt) {
# Get timezone information
dt = "";
for (i=2; i<NF-1; i+=2) {
if ($i == "TZID") {
dt = "TZ=\"" $(i+1) "\" ";
break;
}
}
# Get date/datetime
return length($NF) == 8 ?
dt $NF :
dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", $NF);
}
function parse_duration( dt, dta, i, n, a, seps) {
n = split($NF, a, /[PTWHMSD]/, seps);
delete dta;
for (i=2; i<=n; i++) {
if(seps[i] == "W") dta["weeks"] = a[i];
if(seps[i] == "H") dta["hours"] = a[i];
if(seps[i] == "M") dta["minutes"] = a[i];
if(seps[i] == "S") dta["seconds"] = a[i];
if(seps[i] == "D") dta["days"] = a[i];
}
dt = a[1] ? a[1] : "+";
for (i in dta) {
dt = dt " " dta[i] " " i;
}
return dt;
}
function fn(path, n, a) {
n = split(path, a, "/");
return a[n-1] "/" a[n];
}
BEGIN { FS="[:;=]"; OFS="|" }
BEGINFILE { inside = 0; dur = 0; start = "ERROR"; end = "ERROR" }
/^END:VEVENT/ { print start, dur ? start " " end : end, fn(FILENAME, n, a); nextfile }
/^DTSTART/ && inside { start = parse( dt) }
/^DTEND/ && inside { end = parse( dt) }
/^DURATION/ && inside { end = parse_duration( dt, dta, i, n, a, seps); dur = 1 }
/^BEGIN:VEVENT/ { inside = 1 }

View File

@@ -1,23 +1,34 @@
## src/awk/weekview.awk
## Print view of all appointments of the current week.
##
## @assign startofweek: Date of first day in the week
# Functions
# Compose line that will display a day in the week.
#
# @return: Single-line string
function c() {
return CYAN substr($0, index($0, ">") + 1) OFF " " RED "/" OFF
}
# AWK program
BEGIN {
GREEN = "\033[1;32m";
RED = "\033[1;31m";
WHITE = "\033[1;97m";
CYAN = "\033[1;36m";
FAINT = "\033[2m";
OFF = "\033[m";
GREEN = "\033[1;32m"
RED = "\033[1;31m"
CYAN = "\033[1;36m"
OFF = "\033[m"
OFS = "|"
}
/^[0-7] 00:00 -- 00:00/ { dayline = dayline " " c(); next }
/^[0-7] 00:00 -- / { dayline = dayline " <--" $4 " " c(); next }
/^[0-7] [0-9]{2}:[0-9]{2} -- 00:00/ { dayline = dayline " " $2 "--> " c(); next }
/^[0-7] [0-9]{2}:[0-9]{2} -- [0-9]{2}:[0-9]{2}/ { dayline = dayline " " $2 " - " $4 " " c(); next }
/^[0-7]$/ && dayline { print "+", startofweek " +" $0-1 " days", "", dayline; }
/^[0-7] 00:00 -- / { dayline = dayline " " $4 " " c(); next }
/^[0-7] [0-9]{2}:[0-9]{2} -- 00:00/ { dayline = dayline " " $2 " " c(); next }
/^[0-7] [0-9]{2}:[0-9]{2} -- [0-9]{2}:[0-9]{2}/ { dayline = dayline " " $2 " " $4 " " c(); next }
/^[0-7]$/ && dayline { print "+", startofweek " +" $0-1 " days", "", dayline }
/^[0-7]$/ {
cmd = "date -d '" startofweek " +" $0 " days' +\"%a %e %b %Y\"";
cmd | getline dayline;
close(cmd);
cmd = "date -d '" startofweek " +" $0 " days' +\"%a %e %b %Y\""
cmd | getline dayline
close(cmd)
dayline = GREEN dayline ": " OFF
}

104
src/lib/awk/icalendar.awk Normal file
View File

@@ -0,0 +1,104 @@
# Escape string to be used as content in iCalendar files.
#
# @input str: String to escape
# @return: Escaped string
function escape(str)
{
gsub("\\\\", "\\\\", str)
gsub(";", "\\;", 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
}
# Isolate content part of an iCalendar line, and unescape.
#
# @input str: String
# @return: Unescaped content part
function getcontent(str) {
return unescape(substr(str, index(str, ":") + 1))
}
# Time-zone aware parsing of the date/date-time entry at the current record.
#
# @local variables: dt
# @return: date or date-time string that can be used in date (1)
function parse( dt) {
# Get timezone information
for (i=2; i<NF-1; i+=2) {
if ($i == "TZID") {
dt = "TZ=\"" $(i+1) "\" "
break
}
}
# Get date/date-time
return length($NF) == 8 ?
dt $NF :
dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", $NF)
}
# Map iCalendar duration specification into the format to be used in date (1).
#
# @local variables: dt, dta, i, n, a, seps
# @input duration: iCalendar duration string
# @return: relative-date/date-time specification to be used in date (1)
function parse_duration(duration, dt, dta, i, n, a, seps) {
n = split(duration, a, /[PTWHMSD]/, seps)
for (i=2; i<=n; i++) {
if(seps[i] == "W") dta["weeks"] = a[i]
if(seps[i] == "H") dta["hours"] = a[i]
if(seps[i] == "M") dta["minutes"] = a[i]
if(seps[i] == "S") dta["seconds"] = a[i]
if(seps[i] == "D") dta["days"] = a[i]
}
dt = a[1] ? a[1] : "+"
for (i in dta)
dt = dt " " dta[i] " " i
return dt
}

View File

@@ -1,401 +1,169 @@
#!/bin/sh
set -eu
# TODO: Make sensitive to failures. I don't want to miss appointments!
# TODO Ensure safe use of delimiters
err() {
echo "$1" >/dev/tty
}
if [ -z "${FZF_VCAL_USE_EXPORTED:-}" ]; then
# Read configuration
CONFIGFILE="$HOME/.config/fzf-vcal/config"
if [ ! -f "$CONFIGFILE" ]; then
err "Configuration '$CONFIGFILE' not found."
exit 1
fi
# shellcheck source=/dev/null
. "$CONFIGFILE"
if [ -z "${ROOT:-}" ] || [ -z "${SYNC_CMD:-}" ] || [ -z "${COLLECTION_LABELS:-}" ]; then
err "Configuration is incomplete."
exit 1
fi
export ROOT
export SYNC_CMD
export COLLECTION_LABELS
DAY_START=${DAY_START:-8}
DAY_END=${DAY_END:-18}
export DAY_START
export DAY_END
# Tools
if command -v "fzf" >/dev/null; then
FZF="fzf --black"
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=numbers --language=md}
CAT=${CAT:-cat}
export CAT
### AWK SCRIPTS
AWK_LINES=$(
cat <<'EOF'
@@include src/awk/lines.awk
EOF
)
export AWK_LINES
AWK_MERGE=$(
cat <<'EOF'
@@include src/awk/merge.awk
EOF
)
export AWK_MERGE
AWK_PARSE=$(
cat <<'EOF'
@@include src/awk/parse.awk
EOF
)
export AWK_PARSE
AWK_WEEKVIEW=$(
cat <<'EOF'
@@include src/awk/weekview.awk
EOF
)
export AWK_WEEKVIEW
AWK_DAYVIEW=$(
cat <<'EOF'
@@include src/awk/dayview.awk
EOF
)
export AWK_DAYVIEW
AWK_GET=$(
cat <<'EOF'
@@include src/awk/get.awk
EOF
)
export AWK_GET
AWK_UPDATE=$(
cat <<'EOF'
@@include src/awk/update.awk
EOF
)
export AWK_UPDATE
AWK_NEW=$(
cat <<'EOF'
@@include src/awk/new.awk
EOF
)
export AWK_NEW
AWK_CAL=$(
cat <<'EOF'
@@include src/awk/cal.awk
EOF
)
export AWK_CAL
### END OF AWK SCRIPTS
## Colors
export GREEN="\033[1;32m"
export RED="\033[1;31m"
export WHITE="\033[1;97m"
export CYAN="\033[1;36m"
export ITALIC="\033[3m"
export FAINT="\033[2m"
export OFF="\033[m"
export FZF_VJOUR_USE_EXPORTED="yes"
fi
__load_approx_data() {
find "$ROOT" -type f -name '*.ics' -print0 |
xargs -0 -P0 \
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_LINES"
}
__load_weeks() {
dates=$(awk -F'|' '{ print $2; print $3 }' "$APPROX_DATA_FILE")
file_dates=$(mktemp)
echo "$dates" | date --file="/dev/stdin" +"%s" >"$file_dates"
awk "$AWK_MERGE" "$file_dates" "$APPROX_DATA_FILE"
rm "$file_dates"
}
__show_day() {
weeknr=$(date -d "$DISPLAY_DATE" +"%s")
weeknr=$(((weeknr - 259200) / 604800)) # shift, because epoch origin is a Thursday
files=$(grep "^$weeknr " "$WEEKLY_DATA_FILE" | cut -d " " -f 2-)
# Find relevant files in list of week files
sef=$({
set -- $files
for file in "$@"; do
file="$ROOT/$file"
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_PARSE" "$file"
done
})
if [ -n "$sef" ]; then
today=$(date -d "$DISPLAY_DATE" +"%D")
sef=$(echo "$sef" | while IFS= read -r line; do
set -- $line
starttime="$1"
shift
endtime="$1"
shift
fpath="$(echo "$1" | sed 's/|/ /g')" # we will use | as delimiter (need to convert back!)
shift
description="$(echo "$*" | sed 's/|/:/g')" # we will use | as delimiter
#
daystart=$(date -d "$today 00:00:00" +"%s")
dayend=$(date -d "$today 23:59:59" +"%s")
line=""
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
s=$(date -d "@$starttime" +"%R")
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
s="00:00"
else
continue
fi
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
e=$(date -d "@$endtime" +"%R")
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
e="00:00"
else
continue
fi
echo "$s|$e|$starttime|$endtime|$fpath|$description"
done)
fi
echo "$sef" | sort -n | awk -v daystart="$DAY_START" -v dayend="$DAY_END" "$AWK_DAYVIEW"
}
__list() {
weeknr=$(date -d "$DISPLAY_DATE" +"%s")
weeknr=$(((weeknr - 259200) / 604800)) # shift, because epoch origin is a Thursday
files=$(grep "^$weeknr " "$WEEKLY_DATA_FILE" | cut -d " " -f 2-)
dayofweek=$(date -d "$DISPLAY_DATE" +"%u")
delta=$((1 - dayofweek))
startofweek=$(date -d "$DISPLAY_DATE -$delta days" +"%D")
# loop over files
sef=$({
set -- $files
for file in "$@"; do
file="$ROOT/$file"
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_PARSE" "$file"
done
})
if [ -n "$sef" ]; then
sef=$(echo "$sef" | while IFS= read -r line; do
set -- $line
starttime="$1"
shift
endtime="$1"
shift
#fpath="$1"
shift
description="$*"
for i in $(seq 0 7); do
daystart=$(date -d "$startofweek +$i days 00:00:00" +"%s")
dayend=$(date -d "$startofweek +$i days 23:59:59" +"%s")
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
s=$(date -d "@$starttime" +"%H:%M")
s="$s -"
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
s="00:00 -"
else
continue
fi
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
e=$(date -d "@$endtime" +"%H:%M")
e="- $e"
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
e="- 00:00"
else
continue
fi
echo "$i $s$e >$description"
done
done)
fi
sef=$({
echo "$sef"
seq 0 7
} | sort -n)
echo "$sef" | awk -v startofweek="$startofweek" "$AWK_WEEKVIEW"
#seq -f "$startofweek +%g days" 0 6 |
# LC_ALL=c xargs -I {} date -d "{}" +"%a %e %b %Y"
}
__canonical_datetime_hm() {
s="$1"
t=$(date -d "@$s" +"%R")
dfmt="%F"
if [ "$t" != "00:00" ]; then
dfmt="$dfmt %R"
fi
date -d "@$s" +"$dfmt"
}
__canonical_datetime() {
s="$1"
shift
t=$(date -d "@$s" +"%R")
dfmt="$*%e %b %Y"
if [ "$t" != "00:00" ]; then
dfmt="$dfmt %R %Z"
fi
date -d "@$s" +"$dfmt"
}
__edit() {
start=$(__canonical_datetime_hm "$1")
end=$(__canonical_datetime_hm "$2")
fpath="$3"
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$fpath")
description=$(awk -v field="DESCRIPTION" "$AWK_GET" "$fpath")
filetmp=$(mktemp --suffix='.md')
(
echo "::: |> $start"
echo "::: <| $end"
echo "# $summary"
echo ""
echo "$description"
) >"$filetmp"
checksum=$(cksum "$filetmp")
$EDITOR "$filetmp" >/dev/tty
# Update only if changes are detected
if [ "$checksum" != "$(cksum "$filetmp")" ]; then
filenew="$filetmp.ics"
awk "$AWK_UPDATE" "$filetmp" "$fpath" >"$filenew"
mv "$filenew" "$fpath"
__refresh_data
fi
rm "$filetmp"
}
__refresh_data() {
if [ -n "${APPROX_DATA_FILE:-}" ]; then
rm "$APPROX_DATA_FILE"
fi
if [ -n "${WEEKLY_DATA_FILE:-}" ]; then
rm "$WEEKLY_DATA_FILE"
fi
APPROX_DATA_FILE=$(mktemp)
__load_approx_data >"$APPROX_DATA_FILE"
export APPROX_DATA_FILE
WEEKLY_DATA_FILE=$(mktemp)
__load_weeks >"$WEEKLY_DATA_FILE"
export WEEKLY_DATA_FILE
}
## Start
if [ "${1:-}" = "--help" ]; then
echo "Usage: $0 [OPTION]"
echo ""
echo "You may specify at most one option."
echo " --help Show this help and exit"
echo " --new Create new entry"
echo " --today Show today's appointments"
echo " --goto Interactively enter date to jump to"
echo " --day <day> Show appointments of specified day"
echo " --date <date> Show week of specified date"
echo ""
echo "You may also start this program with setting locale and timezone"
echo "information. For instance, to see and modify all of your calendar"
echo "entries from the perspective of Saigon, run"
echo "TZ='Asia/Saigon' $0"
echo "Likewise, you may specify the usage of Greek with"
echo "LC_TIME=el_GR.UTF-8 $0"
cat <<EOF
Usage: $0 [OPTION]
You may specify at most one of the following options:
--help Show this help and exit
--today Show today's appointments
--yesterday Show yesterday's appointments
--tomorrow Show tomorrow's appointments
--goto Interactively enter date to jump to
--new [date/date-time] Create new entry (today)
--day [date] Show appointments of specified day (today)
--week [date] Show week of specified date (today)
--import file Import iCalendar file
--import-ni file Import iCalendar file non-interactively
--git cmd Run git command cmd relative to calendar root
--git-init Enable the use of git
You may also start this program with setting locale and timezone information.
For instance, to see and modify all of your calendar entries from the
perspective of Saigon, run
TZ='Asia/Saigon' $0
Likewise, you may see your calendar in the Greek language with
LC_TIME=el_GR.UTF-8 $0
EOF
exit
fi
if [ "${1:-}" = "--today" ]; then
exec $0 --day "today"
fi
# Theme
. "sh/theme.sh"
if [ "${1:-}" = "--goto" ]; then
# Misc helper functions
. "sh/misc.sh"
# Preview utilities
. "sh/preview.sh"
# Preview command-line options
. "sh/clipreview.sh"
# View utilities
. "sh/view.sh"
# Reloading command-line options
. "sh/clireload.sh"
# Configuration
. "sh/config.sh"
# Access to awk scripts
. "sh/awkscripts.sh"
# Functions to load calendar data
. "sh/load.sh"
# Functions to modify iCalendar files
. "sh/icalendar.sh"
# Extra, run-and-exit command-line options
. "sh/cliextra.sh"
### Start
__refresh_data
### Exports
# The preview calls run in subprocesses. These require the following variables:
export ROOT CAT AWK_GET AWK_CALSHIFT AWK_CALANNOT CYAN STRIKE FAINT WHITE ITALIC OFF AWK_ATTACHLS
# The reload commands also run in subprocesses, and use in addition
export COLLECTION_LABELS DAY_START DAY_END AWK_DAYVIEW AWK_WEEKVIEW AWK_PARSE
# as well as the following variables that will be dynamically specified. So, we
# export them in the main loop using the following function.
# __export()
# Re-export dynamical variables to subshells.
__export() {
DISPLAY_DATE=$(date -R -d "$DISPLAY_DATE")
export DISPLAY_DATE WEEKLY_DATA_FILE APPROX_DATA_FILE
if [ -n "${TZ:-}" ]; then
export TZ
fi
}
###
### Main loop with the command-line argument
### --today
### --yesterday
### --tomorrow
### --goto
### --new <optional date/date-time argument>
### --day <optional date/date-time argument>
### --week <optional date/date-time argument>
### --set-tz
###
### The command-line argument defaults to "--week today".
while true; do
export DISPLAY_DATE WEEKLY_DATA_FILE APPROX_DATA_FILE
case "${1:-}" in
--today | --yesterday | --tomorrow | --goto | --new | --day | --week | --set-tz) ;;
*)
DISPLAY_DATE="today"
set -- "--week" "$DISPLAY_DATE"
;;
esac
if [ "$1" = "--today" ]; then
DISPLAY_DATE="today"
set -- "--day" "$DISPLAY_DATE"
fi
if [ "$1" = "--yesterday" ]; then
DISPLAY_DATE="yesterday"
set -- "--day" "$DISPLAY_DATE"
fi
if [ "$1" = "--tomorrow" ]; then
DISPLAY_DATE="tomorrow"
set -- "--day" "$DISPLAY_DATE"
fi
if [ "$1" = "--goto" ]; then
DISPLAY_DATE=""
while [ -z "$DISPLAY_DATE" ]; do
while [ -z "${DISPLAY_DATE:-}" ]; do
printf "Enter date you want to jump to, e.g., today + 1 month or 2024-1-14: " >/dev/tty
read -r tmp
if date -d "$tmp"; then
DISPLAY_DATE="$tmp"
if date -d "$tmp" >/dev/null; then
DISPLAY_DATE="$(date -d "$tmp" +"%D")"
fi
done
fi
if [ "${1:-}" = "--new" ]; then
collection=$(echo "$COLLECTION_LABELS" | tr ';' '\n' | awk '/./ {print}' | $FZF --margin="30%" --no-info --delimiter='=' --with-nth=2 --accept-nth=1)
fpath=""
while [ -f "$fpath" ] || [ -z "$fpath" ]; do
uuid=$($UUIDGEN)
fpath="$ROOT/$collection/$uuid.ics"
done
startsec=$(date -d "${2:-today 8:00}" +"%s")
endsec=$((startsec + 3600))
start=$(__canonical_datetime_hm "$startsec")
end=$(__canonical_datetime_hm "$endsec")
filetmp=$(mktemp --suffix='.md')
(
echo "::: |> $start"
echo "::: <| $end"
echo "# <!-- write summary here -->"
echo ""
) >"$filetmp"
checksum=$(cksum "$filetmp")
$EDITOR "$filetmp" >/dev/tty
# Update only if changes are detected
if [ "$checksum" != "$(cksum "$filetmp")" ]; then
filenew="$filetmp.ics"
awk -v uid="$uuid" "$AWK_NEW" "$filetmp" >"$filenew"
mv "$filenew" "$fpath"
__refresh_data
set -- "--day" "$DISPLAY_DATE"
fi
rm "$filetmp"
fi
if [ -z "${APPROX_DATA_FILE:-}" ]; then
if [ "$1" = "--set-tz" ]; then
new_tz=$(find "$ZI_DIR" -type f | sed "s|^$ZI_DIR/*||" | $FZF)
if [ -n "$new_tz" ]; then
TZ="$new_tz"
__refresh_data
fi
__export
fi
shift
fi
if [ "${1:-}" = "--day" ]; then
if [ "${1:-}" = "--new" ]; then
__new "${2:-}"
if [ -n "$start" ]; then
DISPLAY_DATE="$start"
else
DISPLAY_DATE="${2:-}"
fi
__refresh_data
__export
set -- "--day" "$DISPLAY_DATE"
fi
if [ "$1" = "--day" ]; then
DISPLAY_DATE="${2:-today}"
export DISPLAY_DATE
__export
selection=$(
__show_day |
__view_day |
$FZF \
--reverse \
--ansi \
@@ -403,161 +171,136 @@ if [ "${1:-}" = "--day" ]; then
--no-input \
--margin='20%,5%' \
--border='double' \
--border-label="🗓️ $(date -d "$DISPLAY_DATE" +"%A %e %B %Y")" \
--color=label:bold:green \
--border-label-pos=3 \
--list-border="top" \
--list-label-pos=3 \
--cycle \
--delimiter='|' \
--with-nth='{5}' \
--accept-nth='1,2,3,4' \
--preview="$0 --preview {}" \
--expect="ctrl-n,esc,backspace,q" \
--bind='start:hide-preview' \
--bind='ctrl-j:down+hide-preview+transform:echo {} | grep \|\| || echo show-preview' \
--bind='ctrl-k:up+hide-preview+transform:echo {} | grep \|\| || echo show-preview' \
--with-nth='{6}' \
--accept-nth='1,2,3,4,5' \
--preview="$0 --preview-event {}" \
--expect="ctrl-n,ctrl-t,ctrl-g,ctrl-alt-d,esc,backspace,q,alt-v,x,c,a" \
--bind="load:pos(1)+transform(
echo change-border-label:🗓️ \$(date -d {1} +\"%A %e %B %Y\")
)+transform(
[ -n \"\${TZ:-}\" ] && echo \"change-list-label:\$WHITE\$ITALIC(\$TZ)\$OFF\"
)+transform(
[ -n \"\$(echo {} | cut -d '|' -f 5)\" ] && echo show-preview
)" \
--bind="start:hide-preview" \
--bind="j:down+hide-preview+transform([ -n \"\$(echo {} | cut -d '|' -f 5)\" ] && echo show-preview)" \
--bind="k:up+hide-preview+transform([ -n \"\$(echo {} | cut -d '|' -f 5)\" ] && echo show-preview)" \
--bind="ctrl-j:down+hide-preview+transform([ -n \"\$(echo {} | cut -d '|' -f 5)\" ] && echo show-preview)" \
--bind="ctrl-k:up+hide-preview+transform([ -n \"\$(echo {} | cut -d '|' -f 5)\" ] && echo show-preview)" \
--bind="down:down+hide-preview+transform([ -n \"\$(echo {} | cut -d '|' -f 5)\" ] && echo show-preview)" \
--bind="up:up+hide-preview+transform([ -n \"\$(echo {} | cut -d '|' -f 5)\" ] && echo show-preview)" \
--bind="l:hide-preview+reload:$0 --reload-day {1} '+1 day'" \
--bind="h:hide-preview+reload:$0 --reload-day {1} '-1 day'" \
--bind="right:hide-preview+reload:$0 --reload-day {1} '+1 day'" \
--bind="left:hide-preview+reload:$0 --reload-day {1} '-1 day'" \
--bind="ctrl-l:hide-preview+reload:$0 --reload-day {1} '+1 week'" \
--bind="ctrl-h:hide-preview+reload:$0 --reload-day {1} '-1 week'" \
--bind="alt-l:hide-preview+reload:$0 --reload-day {1} '+1 month'" \
--bind="alt-h:hide-preview+reload:$0 --reload-day {1} '-1 month'" \
--bind="ctrl-r:hide-preview+reload:$0 --reload-day today" \
--bind="ctrl-s:execute($SYNC_CMD ; printf 'Press <enter> to continue.'; read -r tmp)" \
--bind="ctrl-alt-d:become($0 --delete {})" \
--bind="j:preview-down" \
--bind="k:preview-down" \
--bind="w:toggle-preview-wrap"
--bind="w:toggle-preview-wrap" \
--bind="ctrl-d:preview-down" \
--bind="ctrl-u:preview-up"
)
key=$(echo "$selection" | head -1)
line=$(echo "$selection" | tail -1)
if [ "$line" = "$key" ]; then
line=""
fi
hour=$(echo "$line" | cut -d '|' -f 1)
start=$(echo "$line" | cut -d '|' -f 2)
end=$(echo "$line" | cut -d '|' -f 3)
fpath=$(echo "$line" | cut -d '|' -f 4 | sed "s/ /|/g")
DISPLAY_DATE=$(echo "$line" | cut -d '|' -f 1)
hour=$(echo "$line" | cut -d '|' -f 2)
start=$(echo "$line" | cut -d '|' -f 3)
end=$(echo "$line" | cut -d '|' -f 4)
fpath=$(echo "$line" | cut -d '|' -f 5 | sed "s/ /|/g")
if [ "$key" = "ctrl-n" ]; then
if echo "$hour" | grep ":"; then
hour="$DAY_START"
fi
exec $0 --new "$DISPLAY_DATE $hour:00"
elif [ -z "$key" ] && [ -n "$fpath" ]; then
fpath="$ROOT/$fpath"
__edit "$start" "$end" "$fpath"
set -- "--new" "$DISPLAY_DATE $hour:00"
elif [ "$key" = "ctrl-alt-d" ] && [ -n "$fpath" ]; then
__delete "$fpath"
__refresh_data
set -- "--day" "$DISPLAY_DATE"
elif [ "$key" = "ctrl-g" ]; then
set -- "--goto"
elif [ "$key" = "ctrl-t" ]; then
set -- "--set-tz" "--day" "$DISPLAY_DATE"
elif [ "$key" = "esc" ] || [ "$key" = "backspace" ] || [ "$key" = "q" ]; then
set -- "--week" "$DISPLAY_DATE"
elif [ "$key" = "alt-v" ] && [ -f "$ROOT/$fpath" ]; then
$EDITOR "$ROOT/$fpath"
elif [ "$key" = "x" ] && [ -f "$ROOT/$fpath" ]; then
__cancel_toggle "$fpath"
elif [ "$key" = "c" ] && [ -f "$ROOT/$fpath" ]; then
__tentative_toggle "$fpath"
elif [ "$key" = "a" ] && [ -f "$ROOT/$fpath" ]; then
att=$(
awk "$AWK_ATTACHLS" "$ROOT/$fpath" |
$FZF \
--delimiter="\t" \
--accept-nth=1,2,3,4 \
--with-nth="Attachment {1}: \"{2}\" {3} ({5})" \
--no-sort \
--tac \
--margin="30%,30%" \
--border=bold \
--border-label="Attachment View Keys: <enter> open, <ctrl-alt-d> delete, <shift-a> add" \
--expect="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)
if [ "$key" = "ctrl-c" ] ||
[ "$key" = "ctrl-g" ] ||
[ "$key" = "ctrl-q" ] ||
[ "$key" = "ctrl-d" ] ||
[ "$key" = "esc" ] ||
[ "$key" = "q" ] ||
[ "$key" = "backspace" ]; then
continue
fi
fi
if [ "${1:-}" = "--date" ]; then
DISPLAY_DATE="$2"
fi
if [ "${1:-}" = "--preview" ]; then
hour=$(echo "$2" | cut -d '|' -f 1)
start=$(echo "$2" | cut -d '|' -f 2)
end=$(echo "$2" | cut -d '|' -f 3)
fpath=$(echo "$2" | cut -d '|' -f 4 | sed "s/ /|/g")
if [ -n "$hour" ] && [ -n "$fpath" ]; then
fpath="$ROOT/$fpath"
start=$(__canonical_datetime "$start" "%a ")
end=$(__canonical_datetime "$end" "%a ")
echo "${GREEN}From: ${OFF}${CYAN}$start${OFF}"
echo "${GREEN}To: ${OFF}${CYAN}$end${OFF}"
echo ""
awk -v field="DESCRIPTION" "$AWK_GET" "$fpath" | $CAT
if [ "$key" = "A" ]; then
__add_attachment "$fpath"
__refresh_data
continue
fi
exit
fi
month_previous() {
month="$1"
year="$2"
if [ "$month" -eq 1 ]; then
month=12
year=$((year - 1))
else
month=$((month - 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)
if [ -z "$attid" ]; then
# This line should be unreachable
continue
fi
echo "$month $year"
}
month_next() {
month="$1"
year="$2"
if [ "$month" -eq 12 ]; then
month=1
year=$((year + 1))
else
month=$((month + 1))
fi
echo "$month $year"
}
if [ "${1:-}" = "--preview-week" ]; then
sign=$(echo "$2" | cut -d '|' -f 1)
if [ "$sign" = "+" ]; then
startdate=$(echo "$2" | cut -d '|' -f 2)
set -- $(date -d "$startdate" +"%Y %m %d")
year=$1
month=$2
day=$3
set -- $(date -d "today" +"%Y %m %d")
year_cur=$1
month_cur=$2
day_cur=$3
# Previous months
set -- $(month_previous "$month" "$year")
month_pre="$1"
year_pre="$2"
set -- $(month_previous "$month_pre" "$year_pre")
month_pre2="$1"
year_pre2="$2"
# Next months
set -- $(month_next "$month" "$year")
month_nex="$1"
year_nex="$2"
set -- $(month_next "$month_nex" "$year_nex")
month_nex2="$1"
year_nex2="$2"
set -- $(month_next "$month_nex2" "$year_nex2")
month_nex3="$1"
year_nex3="$2"
# Highlight today
if [ "$month_pre2" -eq "$month_cur" ] && [ "$year_pre2" -eq "$year_cur" ]; then
var_pre2=$day_cur
fi
if [ "$month_pre" -eq "$month_cur" ] && [ "$year_pre" -eq "$year_cur" ]; then
var_pre=$day_cur
fi
if [ "$month" -eq "$month_cur" ] && [ "$year" -eq "$year_cur" ]; then
var=$day_cur
fi
if [ "$month_nex" -eq "$month_cur" ] && [ "$year_nex" -eq "$year_cur" ]; then
var_nex=$day_cur
fi
if [ "$month_nex2" -eq "$month_cur" ] && [ "$year_nex2" -eq "$year_cur" ]; then
var_nex2=$day_cur
fi
if [ "$month_nex3" -eq "$month_cur" ] && [ "$year_nex3" -eq "$year_cur" ]; then
var_nex3=$day_cur
fi
# show
(
cal "$month_pre2" "$year_pre2" | awk -v cur="${var_pre2:-}" "$AWK_CAL"
cal "$month_pre" "$year_pre" | awk -v cur="${var_pre:-}" "$AWK_CAL"
cal "$month" "$year" | awk -v cur="${var:-}" -v day="$day" "$AWK_CAL"
cal "$month_nex" "$year_nex" | awk -v cur="${var_nex:-}" "$AWK_CAL"
cal "$month_nex2" "$year_nex2" | awk -v cur="${var_nex2:-}" "$AWK_CAL"
cal "$month_nex3" "$year_nex3" | awk -v cur="${var_nex3:-}" "$AWK_CAL"
) | awk '{ l[NR%8] = l[NR%8] " " $0 } END {for (i in l) if (i>0) print l[i] }'
fi
exit
fi
if [ "${1:-}" = "--delete" ]; then
fpath=$(echo "$2" | cut -d '|' -f 4 | sed "s/ /|/g")
if [ -n "$fpath" ]; then
fpath="$ROOT/$fpath"
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$fpath")
if [ "$key" = "ctrl-alt-d" ]; then
while true; do
printf "Do you want to delete the entry with the title \"%s\"? (yes/no): " "$summary" >/dev/tty
printf "Are you sure you want to delete attachment \"%s\"? (yes/no): " "$attid" >/dev/tty
read -r yn
case $yn in
"yes")
rm -v "$fpath"
filetmp=$(mktemp)
awk -v id="$attid" "$AWK_ATTACHRM" "$ROOT/$fpath" >"$filetmp"
mv "$filetmp" "$ROOT/$fpath"
if [ -n "${GIT:-}" ]; then
$GIT add "$fpath"
$GIT commit -m "Deleted attachment from event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
fi
__refresh_data
break
;;
"no")
@@ -568,30 +311,61 @@ if [ "${1:-}" = "--delete" ]; then
;;
esac
done
continue
fi
if [ "$attenc" != "base64" ]; then
err "Unsupported attachment encoding: $attenc"
read -r tmp
continue
fi
if [ -n "$attname" ]; then
tmpdir=$(mktemp -d)
attpath="$tmpdir/$attname"
elif [ -n "$attfmt" ]; then
attext=$(echo "$attfmt" | cut -d "/" -f 2)
attpath=$(mktemp --suffix="$attext")
else
attpath=$(mktemp)
fi
# Get file and uncode
awk -v id="$attid" "$AWK_ATTACHDD" "$ROOT/$fpath" | base64 -d >"$attpath"
fn=$(file "$attpath")
while true; do
printf "Are you sure you want to open \"%s\"? (yes/no): " "$fn" >/dev/tty
read -r yn
case $yn in
"yes")
$OPEN "$attpath"
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
elif [ -z "$key" ] && [ -n "$fpath" ]; then
__edit "$start" "$end" "$fpath"
set -- "--day" "$DISPLAY_DATE"
fi
__export
fi
__refresh_data
exec $0 --day "$DISPLAY_DATE"
fi
if [ "${1:-}" = "--all" ]; then
cat "$APPROX_DATA_FILE"
exit
fi
DISPLAY_DATE=${DISPLAY_DATE:-today}
DISPLAY_DATE=$(date -d "$DISPLAY_DATE" +"%D")
DISPLAY_POS=$((8 - $(date -d "$DISPLAY_DATE" +"%u")))
if [ "${1:-}" = "--list" ]; then
shift
DISPLAY_DATE=${*:-today}
if [ "${1:-}" = "--week" ]; then
DISPLAY_DATE="${2:-today}"
DISPLAY_POS=$((8 - $(date -d "$DISPLAY_DATE" +"%u")))
__list
exit
fi
selection=$(
__list |
__export
selection=$(
__view_week |
$FZF \
--tac \
--no-sort \
@@ -603,53 +377,55 @@ selection=$(
--ansi \
--gap 1 \
--no-scrollbar \
--no-input \
--info=right \
--info-command="printf \"$(date +"%R %Z")\"" \
--margin="1" \
--info-command="printf \"$(date +"%R %Z")\"; [ -n \"\${TZ:-}\" ] && printf \" (\$TZ)\"" \
--preview-window=up,7,border-bottom \
--preview="$0 --preview-week {}" \
--expect="ctrl-n" \
--bind="ctrl-j:transform:[ \$FZF_POS -le 1 ] &&
echo unbind\(load\)+reload:$0 --list {2} '+1 day'||
echo down" \
--bind="ctrl-k:transform:[ \$FZF_POS -ge 7 ] &&
echo unbind\(load\)+reload:$0 --list {2} '-1 day'||
echo up" \
--bind="change:reload($0 --all)+hide-preview" \
--bind="backward-eof:rebind(load)+reload($0 --list)+show-preview" \
--bind="load:pos($DISPLAY_POS)" \
--bind="ctrl-u:unbind(load)+reload:$0 --list {2} '-1 week'" \
--bind="ctrl-d:unbind(load)+reload:$0 --list {2} '+1 week'" \
--bind="ctrl-alt-u:unbind(load)+reload:$0 --list {2} '-1 month'" \
--bind="ctrl-alt-d:unbind(load)+reload:$0 --list {2} '+1 month'" \
--expect="ctrl-n,ctrl-g,ctrl-t" \
--bind="q:abort" \
--bind="j:down" \
--bind="k:up" \
--bind="l:unbind(load)+reload:$0 --reload-week {2} '+1 week'" \
--bind="h:unbind(load)+reload:$0 --reload-week {2} '-1 week'" \
--bind="right:unbind(load)+reload:$0 --reload-week {2} '+1 week'" \
--bind="left:unbind(load)+reload:$0 --reload-week {2} '-1 week'" \
--bind="ctrl-l:unbind(load)+reload:$0 --reload-week {2} '+1 month'" \
--bind="ctrl-h:unbind(load)+reload:$0 --reload-week {2} '-1 month'" \
--bind="alt-l:unbind(load)+reload:$0 --reload-week {2} '+1 year'" \
--bind="alt-h:unbind(load)+reload:$0 --reload-week {2} '-1 year'" \
--bind="ctrl-r:rebind(load)+reload($0 --reload-week today)+show-preview" \
--bind="ctrl-s:execute($SYNC_CMD ; printf 'Press <enter> to continue.'; read -r tmp)" \
--bind="ctrl-g:become($0 --goto)" \
--bind="ctrl-l:rebind(load)+reload:$0 --list"
)
--bind="/:show-input+unbind(q)+unbind(j)+unbind(k)+unbind(l)+unbind(h)+unbind(ctrl-l)+unbind(ctrl-h)+unbind(alt-l)+unbind(alt-h)+unbind(load)+hide-preview+reload:$0 --reload-all" \
--bind="backward-eof:hide-input+rebind(q)+rebind(j)+rebind(k)+rebind(l)+rebind(h)+rebind(ctrl-l)+rebind(ctrl-h)+rebind(alt-l)+rebind(alt-h)+rebind(load)+show-preview+reload:$0 --reload-week today" \
--bind="esc:clear-query+hide-input+rebind(q)+rebind(j)+rebind(k)+rebind(l)+rebind(h)+rebind(ctrl-l)+rebind(ctrl-h)+rebind(alt-l)+rebind(alt-h)+rebind(load)+show-preview+reload:$0 --reload-week today"
)
key=$(echo "$selection" | head -1)
line=$(echo "$selection" | tail -1)
if [ "$line" = "$key" ]; then
key=$(echo "$selection" | head -1)
line=$(echo "$selection" | tail -1)
if [ "$line" = "$key" ]; then
line=""
fi
sign=$(echo "$line" | cut -d '|' -f 1)
startdate=$(echo "$line" | cut -d '|' -f 2)
if [ "$key" = "ctrl-n" ]; then
# Add new
if [ "$sign" = "~" ]; then
startdate=""
fi
exec $0 --new "${startdate:-today} $DAY_START:00"
fi
if [ -z "$key" ] && [ -z "$line" ]; then
rm "$WEEKLY_DATA_FILE" "$APPROX_DATA_FILE"
return 0
fi
if [ "$sign" = "~" ]; then
exec $0 --date "$startdate"
else
exec $0 --day "$startdate"
fi
echo "Going to end..."
echo "$selection"
echo "STOPPING NOW"
sign=$(echo "$line" | cut -d '|' -f 1)
DISPLAY_DATE=$(echo "$line" | cut -d '|' -f 2)
if [ "$key" = "ctrl-n" ]; then
if [ "$sign" = "~" ]; then
DISPLAY_DATE=""
fi
set -- "--new" "${DISPLAY_DATE:-today} $DAY_START:00"
elif [ "$key" = "ctrl-g" ]; then
set -- "--goto"
elif [ "$key" = "ctrl-t" ]; then
set -- "--set-tz" "$*"
else
if [ "$sign" = "~" ]; then
set -- "--week" "$DISPLAY_DATE"
else
set -- "--day" "$DISPLAY_DATE"
fi
fi
__export
fi
done

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

@@ -0,0 +1,106 @@
# AWK scripts
# - AWK_APPROX: Generate approximate data of all files
# - AWK_CALSHIFT: Shift calendar to start weeks on Mondays
# - AWK_CALANNOT: Annotate calendar
# - AWK_DAYVIEW: Generate view of the day
# - AWK_GET: Print field of iCalendar file
# - AWK_MERGE: Generate list of weeks with associated iCalendar files
# - AWK_NEW: Make new iCalendar file
# - AWK_PARSE: Timezone aware parsing of iCalendar file for day view
# - AWK_SET: Set value of specific field in iCalendar file
# - AWK_UPDATE: Update iCalendar file
# - AWK_WEEKVIEW: Generate view of the week
# - AWK_ATTACHLS: List attachments
# - AWK_ATTACHDD: Store attachment
# - AWK_ATTACHRM: Remove attachment
# - AWK_ATTACH: Add attachment
AWK_APPROX=$(
cat <<'EOF'
@@include awk/approx.awk
EOF
)
AWK_MERGE=$(
cat <<'EOF'
@@include awk/merge.awk
EOF
)
AWK_PARSE=$(
cat <<'EOF'
@@include awk/parse.awk
EOF
)
AWK_WEEKVIEW=$(
cat <<'EOF'
@@include awk/weekview.awk
EOF
)
AWK_DAYVIEW=$(
cat <<'EOF'
@@include awk/dayview.awk
EOF
)
AWK_GET=$(
cat <<'EOF'
@@include awk/get.awk
EOF
)
AWK_UPDATE=$(
cat <<'EOF'
@@include awk/update.awk
EOF
)
AWK_NEW=$(
cat <<'EOF'
@@include awk/new.awk
EOF
)
AWK_CALSHIFT=$(
cat <<'EOF'
@@include awk/calshift.awk
EOF
)
AWK_CALANNOT=$(
cat <<'EOF'
@@include awk/calannot.awk
EOF
)
AWK_SET=$(
cat <<'EOF'
@@include awk/set.awk
EOF
)
AWK_ATTACHLS=$(
cat <<'EOF'
@@include awk/attachls.awk
EOF
)
AWK_ATTACHDD=$(
cat <<'EOF'
@@include awk/attachdd.awk
EOF
)
AWK_ATTACHRM=$(
cat <<'EOF'
@@include awk/attachrm.awk
EOF
)
AWK_ATTACH=$(
cat <<'EOF'
@@include awk/attach.awk
EOF
)

129
src/sh/cliextra.sh Normal file
View File

@@ -0,0 +1,129 @@
# Extra command-line options
# - --import-ni
# - --import
# - --git
# - --git-init
# Import iCalendar file noninteractively
#
# @input $2: Absolute path to iCalendar file
# @input $3: Collection
# @return: On success, returns 0, otherwise 1
if [ "${1:-}" = "--import-ni" ]; then
shift
file="${1:-}"
collection="${2:-}"
if [ ! -f "$file" ]; then
err "File \"$file\" does not exist"
exit 1
fi
for c in $(echo "$COLLECTION_LABELS" | sed "s|=[^;]*;| |g"); do
if [ "$collection" = "$c" ]; then
cexists="yes"
break
fi
done
if [ -n "${cexists:-}" ] && [ -d "$ROOT/$collection" ]; then
__import_to_collection "$file" "$collection"
else
err "Collection \"$collection\" does not exist"
exit 1
fi
exit
fi
# Import iCalendar file.
#
# @input $2: Absolute path to iCalendar file
# @return: On success, returns 0, otherwise 1
if [ "${1:-}" = "--import" ]; then
shift
file="${1:-}"
if [ ! -f "$file" ]; then
err "File \"$file\" does not exist"
return 1
fi
line=$(awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_PARSE" "$file")
set -- $line
startsec="${1:-}"
endsec="${2:-}"
if [ -z "$line" ] || [ -z "$startsec" ] || [ -z "$endsec" ]; then
err "File \"$file\" does not look like an iCalendar file containing an event"
return 1
fi
start=$(__datetime_human_machine "$startsec")
end=$(__datetime_human_machine "$endsec")
location=$(awk -v field="LOCATION" "$AWK_GET" "$file")
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$file")
description=$(awk -v field="DESCRIPTION" "$AWK_GET" "$file")
filetmp=$(mktemp --suffix='.md')
(
echo "::: |> $start"
echo "::: <| $end"
) >"$filetmp"
if [ -n "$location" ]; then
echo "@ $location" >>"$filetmp"
fi
(
echo "# $summary"
echo ""
echo "$description"
) >>"$filetmp"
$CAT "$filetmp" >/dev/tty
while true; do
printf "Do you want to import this entry? (yes/no): " >/dev/tty
read -r yn
case $yn in
"yes")
collection=$(echo "$COLLECTION_LABELS" | tr ';' '\n' | awk '/./ {print}' | $FZF --margin="30%" --no-info --delimiter='=' --with-nth=2 --accept-nth=1)
if [ -z "$collection" ]; then
exit
fi
__import_to_collection "$file" "$collection"
break
;;
"no")
break
;;
*)
echo "Please answer \"yes\" or \"no\"." >/dev/tty
;;
esac
done
rm -f "$filetmp"
exit
fi
# Run git command
#
# @input $2..: Git command
# @return: On success, returns 0, otherwise 1
if [ "${1:-}" = "--git" ]; then
if [ -z "${GIT:-}" ]; then
err "Git not supported, run \`$0 --git-init\` first"
return 1
fi
shift
$GIT "$@"
exit
fi
# Enable the ues of git
#
# @return: On success, returns 0, otherwise 1
if [ "${1:-}" = "--git-init" ]; then
if [ -n "${GIT:-}" ]; then
err "Git already enabled"
return 1
fi
if ! command -v "git" >/dev/null; then
err "Git command not found"
return 1
fi
git -C "$ROOT" init
git -C "$ROOT" add -A
git -C "$ROOT" commit -m 'Initial commit: Start git tracking'
exit
fi

100
src/sh/clipreview.sh Normal file
View File

@@ -0,0 +1,100 @@
# Preview command-line options
# - --preview-event
# - --preview_week
# Print preview of event and exit.
#
# @input $2: Line from day view containing an event
if [ "${1:-}" = "--preview-event" ]; then
hour=$(echo "$2" | cut -d '|' -f 2)
start=$(echo "$2" | cut -d '|' -f 3)
end=$(echo "$2" | cut -d '|' -f 4)
fpath=$(echo "$2" | cut -d '|' -f 5 | sed "s/ /|/g")
if [ -n "$hour" ] && [ -n "$fpath" ]; then
fpath="$ROOT/$fpath"
start=$(datetime_str "$start" "%a ")
end=$(datetime_str "$end" "%a ")
location=$(awk -v field="LOCATION" "$AWK_GET" "$fpath")
status=$(awk -v field="STATUS" "$AWK_GET" "$fpath")
if [ "$status" = "TENTATIVE" ]; then
symb="🟡"
elif [ "$status" = "CANCELLED" ]; then
symb="❌"
fi
echo "📅${symb:-} ${CYAN}$start${OFF}${CYAN}$end${OFF}"
if [ -n "${location:-}" ]; then
echo "📍 ${CYAN}$location${OFF}"
fi
attcnt=$(awk "$AWK_ATTACHLS" "$fpath" | wc -l)
if [ "$attcnt" -gt 0 ]; then
echo "🔗 $attcnt attachments"
fi
echo ""
awk -v field="DESCRIPTION" "$AWK_GET" "$fpath" | $CAT
fi
exit
fi
# Print preview of week.
#
# @input $2: Line from week view
if [ "${1:-}" = "--preview-week" ]; then
sign=$(echo "$2" | cut -d '|' -f 1)
if [ "$sign" = "+" ]; then
startdate=$(echo "$2" | cut -d '|' -f 2)
set -- $(date -d "$startdate" +"%Y %m %d")
year=$1
month=$2
day=$3
set -- $(date -d "today" +"%Y %m %d")
year_cur=$1
month_cur=$2
day_cur=$3
# Previous months
set -- $(month_previous "$month" "$year")
month_pre="$1"
year_pre="$2"
set -- $(month_previous "$month_pre" "$year_pre")
month_pre2="$1"
year_pre2="$2"
# Next months
set -- $(month_next "$month" "$year")
month_nex="$1"
year_nex="$2"
set -- $(month_next "$month_nex" "$year_nex")
month_nex2="$1"
year_nex2="$2"
set -- $(month_next "$month_nex2" "$year_nex2")
month_nex3="$1"
year_nex3="$2"
# Highlight today
if [ "$month_pre2" -eq "$month_cur" ] && [ "$year_pre2" -eq "$year_cur" ]; then
var_pre2=$day_cur
fi
if [ "$month_pre" -eq "$month_cur" ] && [ "$year_pre" -eq "$year_cur" ]; then
var_pre=$day_cur
fi
if [ "$month" -eq "$month_cur" ] && [ "$year" -eq "$year_cur" ]; then
var=$day_cur
fi
if [ "$month_nex" -eq "$month_cur" ] && [ "$year_nex" -eq "$year_cur" ]; then
var_nex=$day_cur
fi
if [ "$month_nex2" -eq "$month_cur" ] && [ "$year_nex2" -eq "$year_cur" ]; then
var_nex2=$day_cur
fi
if [ "$month_nex3" -eq "$month_cur" ] && [ "$year_nex3" -eq "$year_cur" ]; then
var_nex3=$day_cur
fi
# show
(
cal "$month_pre2" "$year_pre2" | awk "$AWK_CALSHIFT" | awk -v cur="${var_pre2:-}" "$AWK_CALANNOT"
cal "$month_pre" "$year_pre" | awk "$AWK_CALSHIFT" | awk -v cur="${var_pre:-}" "$AWK_CALANNOT"
cal "$month" "$year" | awk "$AWK_CALSHIFT" | awk -v cur="${var:-}" -v day="$day" "$AWK_CALANNOT"
cal "$month_nex" "$year_nex" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex:-}" "$AWK_CALANNOT"
cal "$month_nex2" "$year_nex2" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex2:-}" "$AWK_CALANNOT"
cal "$month_nex3" "$year_nex3" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex3:-}" "$AWK_CALANNOT"
) | awk '{ l[NR%8] = l[NR%8] " " $0 } END {for (i in l) if (i>0) print l[i] }'
fi
exit
fi

31
src/sh/clireload.sh Normal file
View File

@@ -0,0 +1,31 @@
# Command-line Arguments for reloading views
# - --reload-day
# - --reload-week
# - --reload-all
# Reload view of specified day.
#
# @input $2.. (optional): Specification of day, defaults to `today`
if [ "${1:-}" = "--reload-day" ]; then
shift
DISPLAY_DATE=${*:-today}
__view_day
exit
fi
# Reload view of the week containing the specified date.
#
# @input $2.. (optional): Specification of day, defaults to `today`
if [ "${1:-}" = "--reload-week" ]; then
shift
DISPLAY_DATE=${*:-today}
DISPLAY_POS=$((8 - $(date -d "$DISPLAY_DATE" +"%u")))
__view_week
exit
fi
# Reload view of all entries.
if [ "${1:-}" = "--reload-all" ]; then
__view_all
exit
fi

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

@@ -0,0 +1,65 @@
# Load Configuration
# - ROOT: Directory containing the collections
# - COLLECTION_LABELS: Mappings between collections and labels
# - SYNC_CMD (optional): Synchronization command
# - DAY_START (optional): Hour of start of the day (defaults to 8)
# - DAY_END (optional): Hour of end of the day (defaults to 18)
# - EDITOR (optional): Your favorite editor, is usually already exported
# - TZ (optional): Your favorite timezone, usually system's choice
# - LC_TIME (optional): Your favorite locale for date and time
# - ZI_DIR (optional): Location of tzdata, defaults to /usr/share/zoneinfo
CONFIGFILE="$HOME/.config/fzf-vcal/config"
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 is incomplete."
exit 1
fi
SYNC_CMD=${SYNC_CMD:-echo 'Synchronization disabled'}
DAY_START=${DAY_START:-8}
DAY_END=${DAY_END:-18}
ZI_DIR=${ZI_DIR:-/usr/share/zoneinfo/posix}
if [ ! -d "$ZI_DIR" ]; then
err "Could not determine time-zone information"
exit 1
fi
OPEN=${OPEN:-open}
# Check and load required tools
# - FZF: Fuzzy finder `fzf``
# - UUIDGEN: Tool `uuidgen` to generate random uids
# - CAT: `bat` or `batcat` or `cat`
# - GIT: `git` if it exists
#
# The presence of POSIX tools is not checked.
if command -v "fzf" >/dev/null; then
FZF="fzf --black"
else
err "Did not find the command-line fuzzy finder fzf."
exit 1
fi
if command -v "uuidgen" >/dev/null; then
UUIDGEN="uuidgen"
else
err "Did not find the uuidgen command."
exit 1
fi
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=numbers --language=md}
CAT=${CAT:-cat}
if command -v "git" >/dev/null && [ -d "$ROOT/.git" ]; then
GIT="git -C $ROOT"
fi

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

@@ -0,0 +1,256 @@
# iCalendar modification wrapper
# - __edit
# - __new
# - __delete
# - __import_to_collection
# - __cancel_toggle
# - __tentative_toggle
# - __add_attachment
# Edit iCalendar file.
#
# @input $1: Start date/date-time
# @input $2: End date/date-time
# @input $3: Path to iCalendar file (relative to `$ROOT`)
# @req $AWK_GET: Awk script to extract fields from iCalendar file
# @req $AWK_UPDATE: Awk script to update iCalendar file
# @req $EDITOR: Environment variable of your favorite editor
__edit() {
start=$(__datetime_human_machine "$1")
end=$(__datetime_human_machine "$2")
fpath="$ROOT/$3"
location=$(awk -v field="LOCATION" "$AWK_GET" "$fpath")
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$fpath")
description=$(awk -v field="DESCRIPTION" "$AWK_GET" "$fpath")
filetmp=$(mktemp --suffix='.md')
printf "::: |> %s\n::: <| %s\n" "$start" "$end" >"$filetmp"
if [ -n "$location" ]; then
printf "@ %s\n" "$location" >>"$filetmp"
fi
printf "# %s\n\n%s\n" "$summary" "$description" >>"$filetmp"
checksum=$(cksum "$filetmp")
$EDITOR "$filetmp" >/dev/tty
# Update only if changes are detected
if [ "$checksum" != "$(cksum "$filetmp")" ]; then
filenew="$filetmp.ics"
if awk "$AWK_UPDATE" "$filetmp" "$fpath" >"$filenew"; then
mv "$filenew" "$fpath"
if [ -n "${GIT:-}" ]; then
$GIT add "$fpath"
$GIT commit -m "Modified event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
fi
__refresh_data
else
rm -f "$filenew"
err "Failed to edit entry. Press <enter> to continue."
read -r tmp
fi
fi
rm "$filetmp"
}
# Generate new iCalendar file
#
# This function also sets the `$start` variable to the start of the new entry.
# On failure, start will be empty.
#
# If some start has been specified and the nanoseconds are not 0, we assume
# that the user entered "tomorrow" or something like that, and did not
# specify the time. So, we will use the `$DAY_START` time of that date.
# If the user specified a malformed date/date-time, we fail.
#
# @input $1 (optional): Date or datetime, defaults to today.
# @req $COLLECTION_LABELS: Mapping between collections and lables (see configuration)
# @req $UUIDGEN: `uuidgen` command
# @req $ROOT: Path that contains the collections (see configuration)
# @req $EDITOR: Environment variable of your favorite editor
# @req $AWK_GET: Awk script to extract fields from iCalendar file
# @req $AWK_new: Awk script to generate iCalendar file
__new() {
collection=$(echo "$COLLECTION_LABELS" | tr ';' '\n' | awk '/./ {print}' | $FZF --margin="30%" --no-info --delimiter='=' --with-nth=2 --accept-nth=1)
fpath=""
while [ -f "$fpath" ] || [ -z "$fpath" ]; do
uuid=$($UUIDGEN)
fpath="$ROOT/$collection/$uuid.ics"
done
d="today $DAY_START"
if [ -n "${1:-}" ]; then
d="$1"
if [ "$(date -d "$1" +"%N")" -ne 0 ]; then
d="$d $DAY_START:00"
fi
fi
startsec=$(date -d "$d" +"%s")
endsec=$((startsec + 3600))
start=$(__datetime_human_machine "$startsec")
end=$(__datetime_human_machine "$endsec")
filetmp=$(mktemp --suffix='.md')
(
echo "::: |> $start"
echo "::: <| $end"
echo "@ <!-- write location here, optional line -->"
echo "# <!-- write summary here -->"
echo ""
) >"$filetmp"
checksum=$(cksum "$filetmp")
$EDITOR "$filetmp" >/dev/tty
# Update only if changes are detected
if [ "$checksum" != "$(cksum "$filetmp")" ]; then
filenew="$filetmp.ics"
if awk -v uid="$uuid" "$AWK_NEW" "$filetmp" >"$filenew"; then
mv "$filenew" "$fpath"
if [ -n "${GIT:-}" ]; then
$GIT add "$fpath"
$GIT commit -m "Added event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
fi
start=$(awk -v field="DTSTART" "$AWK_GET" "$fpath" | grep -o '[0-9]\{8\}')
else
rm -f "$filenew"
start=""
err "Failed to create new entry. Press <enter> to continue."
read -r tmp
fi
fi
rm "$filetmp"
}
# Delete iCalendar file
#
# @input $1: Path to iCalendar file, relative to `$ROOT`
# @req $ROOT: Path that contains the collections (see configuration)
# @req $AWK_GET: Awk script to extract fields from iCalendar file
__delete() {
fpath="$ROOT/$1"
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$fpath")
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")
sfg="$(__summary_for_commit "$fpath")"
rm -v "$fpath"
if [ -n "${GIT:-}" ]; then
$GIT add "$fpath"
$GIT commit -m "Deleted event '$sfg ...'" -- "$fpath"
fi
break
;;
"no")
break
;;
*)
echo "Please answer \"yes\" or \"no\"." >/dev/tty
;;
esac
done
}
# Import iCalendar file to specified collection. The only modification made to
# the file is setting the UID.
#
# @input $1: path to iCalendar file
# @input $2: collection name
# @req $ROOT: Path that contains the collections (see configuration)
# @req $UUIDGEN: `uuidgen` command
# @req $AWK_SET: Awk script to set field value
__import_to_collection() {
file="$1"
collection="$2"
fpath=""
while [ -f "$fpath" ] || [ -z "$fpath" ]; do
uuid=$($UUIDGEN)
fpath="$ROOT/$collection/$uuid.ics"
done
filetmp=$(mktemp)
awk -v field="UID" -v value="$uuid" "$AWK_SET" "$file" >"$filetmp"
mv "$filetmp" "$fpath"
if [ -n "${GIT:-}" ]; then
$GIT add "$fpath"
$GIT commit -m "Imported event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
fi
}
# Set status of appointment to CANCELLED or CONFIRMED (toggle)
#
# @input $1: path to iCalendar file
# @req $ROOT: Path that contains the collections (see configuration)
# @req $AWK_SET: Awk script to set field value
# @req $AWK_GET: Awk script to extract fields from iCalendar file
__cancel_toggle() {
fpath="$ROOT/$1"
status=$(awk -v field="STATUS" "$AWK_GET" "$fpath")
newstatus="CANCELLED"
if [ "${status:-}" = "$newstatus" ]; then
newstatus="CONFIRMED"
fi
filetmp=$(mktemp)
awk -v field="STATUS" -v value="$newstatus" "$AWK_SET" "$fpath" >"$filetmp"
mv "$filetmp" "$fpath"
if [ -n "${GIT:-}" ]; then
$GIT add "$fpath"
$GIT commit -m "Event '$(__summary_for_commit "$fpath") ...' has now status $status" -- "$fpath"
fi
}
# Toggle status flag: CONFIRMED <-> TENTATIVE
#
# @input $1: path to iCalendar file
# @req $ROOT: Path that contains the collections (see configuration)
# @req $AWK_SET: Awk script to set field value
# @req $AWK_GET: Awk script to extract fields from iCalendar file
__tentative_toggle() {
fpath="$ROOT/$1"
status=$(awk -v field="STATUS" "$AWK_GET" "$fpath")
newstatus="TENTATIVE"
if [ "${status:-}" = "$newstatus" ]; then
newstatus="CONFIRMED"
fi
filetmp=$(mktemp)
awk -v field="STATUS" -v value="$newstatus" "$AWK_SET" "$fpath" >"$filetmp"
mv "$filetmp" "$fpath"
if [ -n "${GIT:-}" ]; then
$GIT add "$fpath"
$GIT commit -m "Event '$(__summary_for_commit "$fpath") ...' has now status $status" -- "$fpath"
fi
}
# Prepend attachment to iCalendar file
#
# @input $1: path to iCalendar file
# @req $ROOT: Path that contains the collections (see configuration)
# @req $FZF: Fuzzy finder
# @req $AWK_ATTACH: Awk script to add attachment
__add_attachment() {
fpath="$ROOT/$1"
sel=$(
$FZF --prompt="Select attachment> " \
--walker="file,hidden" \
--walker-root="$HOME" \
--expect="ctrl-c,ctrl-g,ctrl-q,esc"
)
key=$(echo "$sel" | head -1)
f=$(echo "$sel" | tail -1)
if [ -n "$key" ]; then
f=""
fi
if [ -z "$f" ] || [ ! -f "$f" ]; then
return
fi
filename=$(basename "$f")
mime=$(file -b -i "$f" | cut -d ';' -f 1)
if [ -z "$mime" ]; then
mime="application/octet-stream"
fi
fenc=$(mktemp)
base64 "$f" >"$fenc"
filetmp=$(mktemp)
awk -v file="$fenc" -v mime="$mime" -v filename="$filename" "$AWK_ATTACH" "$fpath" >"$filetmp"
mv "$filetmp" "$fpath"
if [ -n "${GIT:-}" ]; then
$GIT add "$fpath"
$GIT commit -m "Added attachment to '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
fi
rm "$fenc"
}

41
src/sh/load.sh Normal file
View File

@@ -0,0 +1,41 @@
# Loading functions
# - __load_approx_data
# - __load_weeks
# - __refresh_data
# Print approximate data from iCalendar files in `$ROOT`
__load_approx_data() {
find "$ROOT" -type f -name '*.ics' -print0 |
xargs -0 -P0 \
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_APPROX"
}
# For every relevant week, print associated iCalendar files
__load_weeks() {
dates=$(awk -F'|' '{ print $2; print $3 }' "$APPROX_DATA_FILE")
file_dates=$(mktemp)
echo "$dates" | date --file="/dev/stdin" +"%G|%V" >"$file_dates"
awk "$AWK_MERGE" "$file_dates" "$APPROX_DATA_FILE"
rm "$file_dates"
}
# Refresh approximate data and per-week data.
#
# This functions stores the output of `__load_approx_data` in the temporary
# file `$APPROX_DATA_FILE` and the output of `__load_weeks` in the temporary
# file `@WEEKLY_DATA_FILE`.
__refresh_data() {
if [ -n "${APPROX_DATA_FILE:-}" ]; then
rm -f "$APPROX_DATA_FILE"
fi
if [ -n "${WEEKLY_DATA_FILE:-}" ]; then
rm -f "$WEEKLY_DATA_FILE"
fi
APPROX_DATA_FILE=$(mktemp)
__load_approx_data >"$APPROX_DATA_FILE"
WEEKLY_DATA_FILE=$(mktemp)
__load_weeks >"$WEEKLY_DATA_FILE"
trap 'rm -f "$APPROX_DATA_FILE" "$WEEKLY_DATA_FILE"' EXIT INT
}

27
src/sh/misc.sh Normal file
View File

@@ -0,0 +1,27 @@
# err()
# This is a helper function to print errors.
#
# @input $1: Error message
err() {
echo "$1" >/dev/tty
}
# Print date or datetime in a human and machine readable form.
#
# @input $1: Seconds since epoch
__datetime_human_machine() {
s="$1"
t=$(date -d "@$s" +"%R")
dfmt="%F"
if [ "$t" != "00:00" ]; then
dfmt="$dfmt %R"
fi
date -d "@$s" +"$dfmt"
}
# Get summary string that can be used in for git-commit messages.
#
# @input $1: iCalendar file path
__summary_for_commit() {
awk -v field="SUMMARY" "$AWK_GET" "$1" | tr -c -d "[:alnum:][:blank:]" | head -c 15
}

51
src/sh/preview.sh Normal file
View File

@@ -0,0 +1,51 @@
# Preview helper functions
# - month_previous
# - month_next
# - datetime_str
# Print previous month of specified input month as <month> <year>.
#
# @input $1: Month
# @input $2: Year
month_previous() {
month=$(echo "$1" | sed 's/^0//')
year=$(echo "$2" | sed 's/^0//')
if [ "$month" -eq 1 ]; then
month=12
year=$((year - 1))
else
month=$((month - 1))
fi
echo "$month $year"
}
# Print next month of specified input month as <month> <year>.
#
# @input $1: Month
# @input $2: Year
month_next() {
month=$(echo "$1" | sed 's/^0//')
year=$(echo "$2" | sed 's/^0//')
if [ "$month" -eq 12 ]; then
month=1
year=$((year + 1))
else
month=$((month + 1))
fi
echo "$month $year"
}
# Print date or datetime in a human readable form.
#
# @input $1: Seconds since epoch
# @input $2.. (optoinal): Prepend date format
datetime_str() {
s="$1"
shift
t=$(date -d "@$s" +"%R")
dfmt="$*%e %b %Y"
if [ "$t" != "00:00" ]; then
dfmt="$dfmt %R %Z"
fi
date -d "@$s" +"$dfmt"
}

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

@@ -0,0 +1,8 @@
GREEN="\033[1;32m"
RED="\033[1;31m"
WHITE="\033[1;97m"
CYAN="\033[1;36m"
STRIKE="\033[9m"
ITALIC="\033[3m"
FAINT="\033[2m"
OFF="\033[m"

130
src/sh/view.sh Normal file
View File

@@ -0,0 +1,130 @@
# View Functions
# - __view_day
# - __view_week
# - __view_all
# This function prints the view for the day specified in `$DISPLAY_DATE`.
__view_day() {
weeknr=$(date -d "$DISPLAY_DATE" +"%G.%V")
files=$(grep "^$weeknr\ " "$WEEKLY_DATA_FILE" | cut -d " " -f 2-)
# Find relevant files in list of week files
sef=$({
set -- $files
for file in "$@"; do
file="$ROOT/$file"
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_PARSE" "$file"
done
})
today=$(date -d "$DISPLAY_DATE" +"%D")
if [ -n "$sef" ]; then
sef=$(echo "$sef" | while IFS= read -r line; do
set -- $line
starttime="$1"
shift
endtime="$1"
shift
fpath="$(echo "$1" | sed 's/|/ /g')" # we will use | as delimiter (need to convert back!)
shift
collection="$1"
shift
status="$1"
shift
description="$(echo "$*" | sed 's/|/:/g')" # we will use | as delimiter
#
daystart=$(date -d "$today 00:00:00" +"%s")
dayend=$(date -d "$today 23:59:59" +"%s")
line=""
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
s=$(date -d "@$starttime" +"%R")
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
s="00:00"
else
continue
fi
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
e=$(date -d "@$endtime" +"%R")
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
e="00:00"
else
continue
fi
echo "$s|$e|$starttime|$endtime|$fpath|$collection|$description|$status"
done)
fi
echo "$sef" | sort -n | awk -v today="$today" -v daystart="$DAY_START" -v dayend="$DAY_END" "$AWK_DAYVIEW"
}
# This function prints the view for the week that contains the day specified in `$DISPLAY_DATE`.
__view_week() {
weeknr=$(date -d "$DISPLAY_DATE" +"%G.%V")
files=$(grep "^$weeknr\ " "$WEEKLY_DATA_FILE" | cut -d " " -f 2-)
dayofweek=$(date -d "$DISPLAY_DATE" +"%u")
delta=$((1 - dayofweek))
startofweek=$(date -d "$DISPLAY_DATE -$delta days" +"%D")
# loop over files
sef=$({
set -- $files
for file in "$@"; do
file="$ROOT/$file"
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_PARSE" "$file"
done
})
if [ -n "$sef" ]; then
sef=$(echo "$sef" | while IFS= read -r line; do
set -- $line
starttime="$1"
shift
endtime="$1"
shift
#fpath="$1"
shift
collection="$1"
shift
status="$1"
shift
if [ "$status" = "TENTATIVE" ]; then
symb="$FAINT$CYAN"
elif [ "$status" = "CANCELLED" ]; then
symb="$STRIKE"
else
symb=""
fi
description="${symb:-}$*$OFF"
for i in $(seq 0 7); do
daystart=$(date -d "$startofweek +$i days 00:00:00" +"%s")
dayend=$(date -d "$startofweek +$i days 23:59:59" +"%s")
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
s=$(date -d "@$starttime" +"%H:%M")
s="$s -"
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
s="00:00 -"
else
continue
fi
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
e=$(date -d "@$endtime" +"%H:%M")
e="- $e"
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
e="- 00:00"
else
continue
fi
echo "$i $s$e >$description"
done
done)
fi
sef=$({
echo "$sef"
seq 0 7
} | sort -n)
echo "$sef" | awk -v startofweek="$startofweek" "$AWK_WEEKVIEW"
}
# This function prints all entries.
__view_all() {
cat "$APPROX_DATA_FILE"
}