Compare commits

...

44 Commits

Author SHA1 Message Date
6dcaf3fbab lic and readme 2025-06-11 22:19:31 +02:00
e1d26fa2e1 help and improvements 2025-06-11 22:02:50 +02:00
29f1672020 improvements 2025-06-11 16:18:37 +02:00
87a41be6de reload instead of become 2025-06-11 16:09:34 +02:00
e91035c43a improved smootheness 2025-06-11 13:32:03 +02:00
66ca05f744 auto hide preview 2025-06-11 13:22:36 +02:00
9e59e7a923 more months preview 2025-06-11 12:28:35 +02:00
afdf8430d0 gaps in week view 2025-06-11 12:18:54 +02:00
b33acff21e improved info line 2025-06-11 10:21:53 +02:00
f463b957bd bug fix, removed prints in awk 2025-06-11 10:18:43 +02:00
e254463b0e bug fix 2025-06-11 10:16:40 +02:00
9291137b94 improved new selector 2025-06-11 10:12:30 +02:00
ad32fcdb0f show tz in info 2025-06-11 10:09:45 +02:00
f025ecd0bb goto functionality 2025-06-11 10:03:04 +02:00
5ebcc4feb6 general improvements in prog flow 2025-06-11 09:56:31 +02:00
3a95c1e162 cal on top 2025-06-11 09:25:38 +02:00
c9759a0fc5 color cal 2025-06-11 09:11:36 +02:00
9558a1d593 cal as preview 2025-06-10 22:33:10 +02:00
7cd8d0ffa8 added quit key 2025-06-10 15:26:55 +02:00
e4fe586db3 minor improvements 2025-06-10 15:11:49 +02:00
6357eddbec new fixes 2025-06-10 15:05:14 +02:00
7a22f73d36 added new new 2025-06-10 14:54:48 +02:00
a32507a7fb improved manouvering 2025-06-10 14:54:36 +02:00
12bde0fa39 better loading 2025-06-10 14:51:43 +02:00
5d8b913d78 delete functionality 2025-06-10 14:37:07 +02:00
8824959715 added --new functionality with keybnding 2025-06-10 14:29:55 +02:00
c7f5082671 added expect 2025-06-10 13:56:27 +02:00
cf19b724ea better keys 2025-06-10 13:30:41 +02:00
6e2df59c93 imporved fzf dayview info 2025-06-10 13:28:11 +02:00
f85e766a5a better fzf 2025-06-10 13:26:01 +02:00
af5356f07b configurable start and end of day 2025-06-10 13:13:43 +02:00
54b10ad726 clead awk script a bit 2025-06-10 13:08:45 +02:00
5586619abf fix day view 2025-06-10 13:06:00 +02:00
801108c70c update functionality 2025-06-09 22:13:22 +02:00
7ea35e539e canonical datetime and some todos 2025-06-09 21:07:58 +02:00
7e9ab45492 layout 2025-06-06 15:59:50 +02:00
3961234b12 fixes 2025-06-06 15:48:47 +02:00
b5682bf435 preview 2025-06-06 10:54:38 +02:00
1fd35de475 some initial previewk lots ofs fixes 2025-06-06 10:44:21 +02:00
19c65d7ce1 improved dayview 2025-06-05 23:02:35 +02:00
e5134fd0a0 improved 2025-06-05 23:02:25 +02:00
558f8330ca switch to day view 2025-06-05 22:56:03 +02:00
08adcd2f98 hook for day view 2025-06-05 20:11:27 +02:00
aaf3c56565 cleand and with-nth 2025-06-05 20:05:58 +02:00
12 changed files with 882 additions and 62 deletions

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright © 2025 Ämin Baumeler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

103
README.md Normal file
View File

@@ -0,0 +1,103 @@
A [fzf](https://github.com/junegunn/fzf)-based **calendar** application with CalDav support.
Description and Use Case
------------------------
This application allows for a keyboard-controlled maneuvering of your calendar entries.
These entries are stored as [iCalendar](https://datatracker.ietf.org/doc/html/rfc5545) files of the type `VEVENT`.
For instance, you could use this application in a setup with a CalDav server,
such as [Radicale](https://radicale.org/), and a synchronization tool like
[vdirsyncer](http://vdirsyncer.pimutils.org/).
Installation
------------
Download the file `fzf-vcal` from the [latest release](https://github.com/baumea/fzf-vcal/releases/latest), or run `./scripts/build.sh`, then
copy `fzf-vcal` to your preferred location, e.g., `~/.local/bin`, and make it executable.
### Requirements
This is a POSIX script with inline `awk` elements.
Make sure you have [fzf](https://github.com/junegunn/fzf) installed.
I also suggest to install [batcat](https://github.com/sharkdp/bat) for colorful previews.
Configuration
--------------
This application is configured with a file located at `$HOME/.config/fzf-vcal/config`.
The entry `ROOT` specifies the root directory of your calendar entries.
This directory may contain several subfolders, called _collections_.
The entry `COLLECTION_LABELS` is a `;`-delimited list, where each item specifies a subfolder and a label (see example below).
In the application, the user sees the collection labels instead of the collection names.
This is particularly useful, because some servers use randomly generated names.
Finally, a third entry `SYNC_CMD` specifies the command to be executed for synchronizing.
Consider the following example:
```sh
ROOT=~/.calendar/
COLLECTION_LABELS="745ae7a0-d723-4cd8-80c4-75f52f5b7d90=👫🏼;12cacb18-d3e1-4ad4-a1d0-e5b209012e85=💼;"
SYNC_CMD="vdirsyncer sync calendar"
```
Here, the files are stored in
`~/.journal/12cacb18-d3e1-4ad4-a1d0-e5b209012e85` (work-related entries)
and
`~/.journal/745ae7a0-d723-4cd8-80c4-75f52f5b7d90` (shared collection).
This configuration will work well with a `vdirsyncer` configuration such as
```confini
[pair calendar]
a = "local"
b = "remote"
collections = ["from a", "from b"]
[storage local]
type = "filesystem"
fileext = ".ics"
path = "~/.calendar"
[storage remote]
type = "caldav"
item_types = ["VEVENT"]
...
```
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.
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 ||
You may also invoke the script with `--help` to see further command-line options.
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.
License
-------
This project is licensed under the [MIT License](./LICENSE).

18
src/awk/cal.awk Normal file
View File

@@ -0,0 +1,18 @@
BEGIN {
BLACK = "\033[1;30m"
GREEN = "\033[1;32m"
RED = "\033[1;31m"
FAINT = "\033[2m"
BOLD = "\033[1m"
BG = "\033[41m"
OFF = "\033[m"
day = day + 0
cur = cur + 0
}
NR == 1 { print GREEN $0 OFF; next }
NR == 2 { print FAINT $0 OFF; next }
{
sub("\\y"cur"\\y", BG BLACK BOLD cur OFF)
sub("\\y"day"\\y", RED BOLD day OFF)
print
}

50
src/awk/dayview.awk Normal file
View File

@@ -0,0 +1,50 @@
# 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
}
function endstoday(stop, desc) {
return CYAN " -- " stop OFF ": " desc
}
function slice(start, stop, desc) {
if (stop == "00:00")
return CYAN start " -- " OFF ": " desc
else
return CYAN start OFF " -- " CYAN stop OFF ": " desc
}
function hrline(hour) {
hour = hour < 10 ? "0"hour : hour
print hour, "", "", "", FAINT hour ":00 ----------------------" OFF
}
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++)
hrline(i)
tmp = substr(stop, 4, 2) == "00" ? 0 : 1
if (stoph + tmp < daystart)
return daystart
else
return stoph + tmp
}
BEGIN {
FS = "|"
GREEN = "\033[1;32m"
RED = "\033[1;31m"
WHITE = "\033[1;97m"
CYAN = "\033[1;36m"
ITALIC = "\033[3m"
FAINT = "\033[2m"
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 ~ /^[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)
}

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

@@ -0,0 +1,18 @@
# print content of field `field`
BEGIN { FS = ":"; regex = "^" field; }
/^BEGIN:VEVENT$/ { inside = 1 }
/^END:VEVENT$/ { exit }
$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;
}

View File

@@ -48,7 +48,7 @@ function title(start, summary) {
depth = split(FILENAME, path, "/"); depth = split(FILENAME, path, "/");
collection = depth > 1 ? path[depth-1] : ""; collection = depth > 1 ? path[depth-1] : "";
collection = collection in collection2label ? collection2label[collection] : collection; collection = collection in collection2label ? collection2label[collection] : collection;
return FAINT "~ " collection " " gensub(/^[^0-9]*([0-9]{4})([0-9]{2}).*$/, "\\1-\\2", "1", start) " " summary " ;" start OFF return FAINT "~ " collection " " gensub(/^[^0-9]*([0-9]{4})([0-9]{2}).*$/, "\\1-\\2", "1", start) " " summary OFF
} }
BEGIN { BEGIN {
@@ -69,7 +69,7 @@ BEGIN {
OFF = "\033[m"; OFF = "\033[m";
} }
BEGINFILE { inside = 0; rs = 0; dur = 0; summary = ""; start = "ERROR"; end = "ERROR" } 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 } /^END:VEVENT/ { print "~", start, dur ? start " " end : end, title(start, summary), fn(FILENAME, n, a); nextfile }
/^DTSTART/ && inside { start = parse( dt) } /^DTSTART/ && inside { start = parse( dt) }
/^DTEND/ && inside { end = parse( dt) } /^DTEND/ && inside { end = parse( dt) }
/^DURATION/ && inside { end = parse_duration( dt, dta, i, n, a, seps); dur = 1 } /^DURATION/ && inside { end = parse_duration( dt, dta, i, n, a, seps); dur = 1 }

View File

@@ -11,7 +11,7 @@ NR == FNR {
if (from[FNR] > to[FNR]) if (from[FNR] > to[FNR])
print "FNR", FNR, ":", from[FNR],"-",to[FNR], " ",$0; print "FNR", FNR, ":", from[FNR],"-",to[FNR], " ",$0;
for(i=from[FNR]; i<=to[FNR]; i++) { for(i=from[FNR]; i<=to[FNR]; i++) {
week[i] = week[i] ? week[i] ";" $4 : $4 week[i] = week[i] " " $5
} }
} }
END { for (i in week) print i, week[i]; } END { for (i in week) print i week[i]; }

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

@@ -0,0 +1,95 @@
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;
}
}
BEGIN {
FS=":";
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1);
}
desc { desc = desc "\\n" $0; next; }
{
from = substr($0, 1, 6) == "::: |>" ? substr($0, 8) : "";
getline
to = substr($0, 1, 6) == "::: <|" ? substr($0, 8) : "";
getline
summary = substr($0, 1, 2) == "# " ? substr($0, 3) : ""
getline # This line should be empty
getline # First line of description
desc = $0;
next;
}
END {
# Sanitize input
# If nanoseconds are not 0, then we assume user enterd "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\"";
cmd | getline t
close(cmd)
t = t + 0
if (t == 0) {
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
close(cmd)
#
to = to ? to : "now"
cmd = "date -d \"" to "\" +\"%N\"";
cmd | getline t
close(cmd)
t = t + 0
if (t == 0) {
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
close(cmd)
escape(summary);
escape(desc);
# print ical
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 "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);
print "END:VEVENT"
print "END:VCALENDAR"
}

View File

@@ -38,8 +38,10 @@ function print_data(start, dur, end, summary, cmd, collection) {
gsub("\\\\;", ";", summary); gsub("\\\\;", ";", summary);
gsub("\\\\\\\\", "\\", summary); gsub("\\\\\\\\", "\\", summary);
depth = split(FILENAME, path, "/"); depth = split(FILENAME, path, "/");
fpath = path[depth-1] "/" path[depth]
collection = depth > 1 ? path[depth-1] : ""; collection = depth > 1 ? path[depth-1] : "";
collection = collection in collection2label ? collection2label[collection] : collection; collection = collection in collection2label ? collection2label[collection] : collection;
collection = collection2label[path[depth-1]]
end = dur ? start " " end : end end = dur ? start " " end : end
cmd = "date -d '" start "' +\"%s\"" cmd = "date -d '" start "' +\"%s\""
cmd | getline start cmd | getline start
@@ -47,7 +49,7 @@ function print_data(start, dur, end, summary, cmd, collection) {
cmd = "date -d '" end "' +\"%s\"" cmd = "date -d '" end "' +\"%s\""
cmd | getline end cmd | getline end
close(cmd) close(cmd)
print start, end, collection, summary print start, end, fpath, collection, summary
} }
BEGIN { BEGIN {
@@ -58,13 +60,6 @@ BEGIN {
split(mapping[map], m, "="); split(mapping[map], m, "=");
collection2label[m[1]] = m[2]; 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";
} }
/^END:VEVENT/ && inside { print_data(start, dur, end, summary, cmd, collection); exit } /^END:VEVENT/ && inside { print_data(start, dur, end, summary, cmd, collection); exit }
/^DTSTART/ && inside { start = parse( dt) } /^DTSTART/ && inside { start = parse( dt) }

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

@@ -0,0 +1,101 @@
function getcontent(content_line, prop)
{
return substr(content_line[prop], index(content_line[prop], ":") + 1);
}
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;
}
}
BEGIN {
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
# 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\"";
cmd | getline t
close(cmd)
t = t + 0
if (t == 0) {
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
close(cmd)
#
to = to ? to : "now"
cmd = "date -d \"" to "\" +\"%N\"";
cmd | getline t
close(cmd)
t = t + 0
if (t == 0) {
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
close(cmd)
}
escape(summary);
escape(desc);
}
NR == FNR && desc { desc = desc "\\n" $0; next; }
NR == FNR {
from = substr($0, 1, 6) == "::: |>" ? substr($0, 8) : "";
getline
to = substr($0, 1, 6) == "::: <|" ? substr($0, 8) : "";
getline
summary = substr($0, 1, 2) == "# " ? substr($0, 3) : ""
getline # This line should be empty
getline # First line of description
desc = $0;
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)
inside = ""
}
{ print }

View File

@@ -8,12 +8,13 @@ BEGIN {
CYAN = "\033[1;36m"; CYAN = "\033[1;36m";
FAINT = "\033[2m"; FAINT = "\033[2m";
OFF = "\033[m"; OFF = "\033[m";
OFS = "|"
} }
/^[0-7] 00:00 -- 00:00/ { dayline = dayline " " c(); next } /^[0-7] 00:00 -- 00:00/ { dayline = dayline " " c(); next }
/^[0-7] 00:00 -- / { dayline = dayline " <-|" $4 " " 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} -- 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] [0-9]{2}:[0-9]{2} -- [0-9]{2}:[0-9]{2}/ { dayline = dayline " " $2 " - " $4 " " c(); next }
/^[0-7]$/ && dayline { print dayline " ;" startofweek " +" $0 " days"; } /^[0-7]$/ && dayline { print "+", startofweek " +" $0-1 " days", "", dayline; }
/^[0-7]$/ { /^[0-7]$/ {
cmd = "date -d '" startofweek " +" $0 " days' +\"%a %e %b %Y\""; cmd = "date -d '" startofweek " +" $0 " days' +\"%a %e %b %Y\"";
cmd | getline dayline; cmd | getline dayline;

View File

@@ -25,9 +25,14 @@ if [ -z "${FZF_VCAL_USE_EXPORTED:-}" ]; then
export SYNC_CMD export SYNC_CMD
export COLLECTION_LABELS export COLLECTION_LABELS
DAY_START=${DAY_START:-8}
DAY_END=${DAY_END:-18}
export DAY_START
export DAY_END
# Tools # Tools
if command -v "fzf" >/dev/null; then if command -v "fzf" >/dev/null; then
FZF="fzf" FZF="fzf --black"
else else
err "Did not find the command-line fuzzy finder fzf." err "Did not find the command-line fuzzy finder fzf."
exit 1 exit 1
@@ -73,22 +78,59 @@ EOF
) )
export AWK_PARSE export AWK_PARSE
AWK_WEEKS=$(
cat <<'EOF'
@@include src/awk/weeks.awk
EOF
)
export AWK_WEEKS
AWK_WEEKVIEW=$( AWK_WEEKVIEW=$(
cat <<'EOF' cat <<'EOF'
@@include src/awk/weekview.awk @@include src/awk/weekview.awk
EOF EOF
) )
export AWK_WEEKVIEW 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 ### END OF AWK SCRIPTS
FZF_VJOUR_USE_EXPORTED="yes"
export FZF_VJOUR_USE_EXPORTED ## 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 fi
__load_approx_data() { __load_approx_data() {
@@ -100,23 +142,71 @@ __load_approx_data() {
} }
__load_weeks() { __load_weeks() {
dates=$(awk -F'|' '{ print $1; print $2 }' "$APPROX_DATA_FILE") dates=$(awk -F'|' '{ print $2; print $3 }' "$APPROX_DATA_FILE")
file_dates=$(mktemp) file_dates=$(mktemp)
echo "$dates" | date --file="/dev/stdin" +"%s" >"$file_dates" echo "$dates" | date --file="/dev/stdin" +"%s" >"$file_dates"
awk "$AWK_MERGE" "$file_dates" "$APPROX_DATA_FILE" awk "$AWK_MERGE" "$file_dates" "$APPROX_DATA_FILE"
rm "$file_dates" 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() { __list() {
weeknr=$(date -d "$DISPLAY_DATE" +"%s") weeknr=$(date -d "$DISPLAY_DATE" +"%s")
weeknr=$(((weeknr - 259200) / 604800)) # shift, because epoch origin is a Thursday weeknr=$(((weeknr - 259200) / 604800)) # shift, because epoch origin is a Thursday
files=$(grep "^$weeknr " "$WEEKLY_DATA_FILE" | cut -d " " -f 2) files=$(grep "^$weeknr " "$WEEKLY_DATA_FILE" | cut -d " " -f 2-)
dayofweek=$(date -d "$DISPLAY_DATE" +"%u") dayofweek=$(date -d "$DISPLAY_DATE" +"%u")
delta=$((1 - dayofweek)) delta=$((1 - dayofweek))
startofweek=$(date -d "$DISPLAY_DATE -$delta days" +"%D") startofweek=$(date -d "$DISPLAY_DATE -$delta days" +"%D")
# loop over files # loop over files
sef=$({ sef=$({
IFS=';'
set -- $files set -- $files
for file in "$@"; do for file in "$@"; do
file="$ROOT/$file" file="$ROOT/$file"
@@ -132,6 +222,8 @@ __list() {
shift shift
endtime="$1" endtime="$1"
shift shift
#fpath="$1"
shift
description="$*" description="$*"
for i in $(seq 0 7); do for i in $(seq 0 7); do
daystart=$(date -d "$startofweek +$i days 00:00:00" +"%s") daystart=$(date -d "$startofweek +$i days 00:00:00" +"%s")
@@ -165,59 +257,399 @@ __list() {
# LC_ALL=c xargs -I {} date -d "{}" +"%a %e %b %Y" # LC_ALL=c xargs -I {} date -d "{}" +"%a %e %b %Y"
} }
if [ -z "${APPROX_DATA_FILE:-}" ]; then __canonical_datetime_hm() {
echo "GOING TO LOAD" 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) APPROX_DATA_FILE=$(mktemp)
__load_approx_data >"$APPROX_DATA_FILE" __load_approx_data >"$APPROX_DATA_FILE"
export APPROX_DATA_FILE export APPROX_DATA_FILE
fi
if [ -z "${WEEKLY_DATA_FILE:-}" ]; then
echo "GOING TO LOAD WD"
WEEKLY_DATA_FILE=$(mktemp) WEEKLY_DATA_FILE=$(mktemp)
__load_weeks >"$WEEKLY_DATA_FILE" __load_weeks >"$WEEKLY_DATA_FILE"
export 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"
exit
fi
if [ "${1:-}" = "--today" ]; then
exec $0 --day "today"
fi
if [ "${1:-}" = "--goto" ]; then
DISPLAY_DATE=""
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"
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
fi
rm "$filetmp"
fi
if [ -z "${APPROX_DATA_FILE:-}" ]; then
__refresh_data
fi
if [ "${1:-}" = "--day" ]; then
DISPLAY_DATE="${2:-today}"
export DISPLAY_DATE
selection=$(
__show_day |
$FZF \
--reverse \
--ansi \
--no-sort \
--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 \
--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' \
--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"
)
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")
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"
fi
fi fi
DISPLAY_DATE="today"
if [ "${1:-}" = "--date" ]; then if [ "${1:-}" = "--date" ]; then
DISPLAY_DATE="$2" DISPLAY_DATE="$2"
echo "Jumping to date $2!"
fi fi
DISPLAY_DATE=$(date -d "$DISPLAY_DATE" +"%D")
DISPLAY_DATE_PREV=$(date -d "$DISPLAY_DATE -1 week" +"%D")
DISPLAY_DATE_NEXT=$(date -d "$DISPLAY_DATE +1 week" +"%D")
selection=$( ( if [ "${1:-}" = "--preview" ]; then
cut -d '|' -f 3 "$APPROX_DATA_FILE" hour=$(echo "$2" | cut -d '|' -f 1)
yes " " | head -n 50 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
fi
exit
fi
month_previous() {
month="$1"
year="$2"
if [ "$month" -eq 1 ]; then
month=12
year=$((year - 1))
else
month=$((month - 1))
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")
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 "$fpath"
break
;;
"no")
break
;;
*)
echo "Please answer \"yes\" or \"no\"." >/dev/tty
;;
esac
done
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}
DISPLAY_POS=$((8 - $(date -d "$DISPLAY_DATE" +"%u")))
__list __list
) | exit
fi
selection=$(
__list |
$FZF \ $FZF \
--tac \ --tac \
--no-sort \ --no-sort \
--no-hscroll \ --no-hscroll \
--ellipsis='' \ --ellipsis="" \
--delimiter="|" \
--with-nth="{4}" \
--accept-nth=1,2 \
--ansi \ --ansi \
--no-clear \ --gap 1 \
--bind="ctrl-p:become($0 --date '$DISPLAY_DATE_PREV')" \ --no-scrollbar \
--bind="ctrl-n:become($0 --date '$DISPLAY_DATE_NEXT')" \ --info=right \
--bind="ctrl-l:become($0)") --info-command="printf \"$(date +"%R %Z")\"" \
--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'" \
--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"
)
if [ -z "$selection" ]; 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" rm "$WEEKLY_DATA_FILE" "$APPROX_DATA_FILE"
return 0 return 0
fi fi
case "$selection" in if [ "$sign" = "~" ]; then
"~"*) exec $0 --date "$startdate"
start=$(echo "$selection" | rev | cut -d';' -f 1 | rev) else
exec $0 --date "$start" exec $0 --day "$startdate"
;; fi
*)
day=$(echo "$selection" | rev | cut -d';' -f 1 | rev)
exec $0 --day "$day"
;;
esac
echo "Going to end..." echo "Going to end..."
echo "$selection" echo "$selection"
echo "STOPPING NOW" echo "STOPPING NOW"