Compare commits

...

16 Commits

Author SHA1 Message Date
498c7371b7 externalize awk scripts 2025-06-02 09:19:28 +02:00
8d5223343b fix:repeated STATUS 2025-05-28 16:09:39 +02:00
f7d4a54a3d fix:wrong config path 2025-05-28 14:24:17 +02:00
3db94cf627 readme update 2025-05-28 14:22:11 +02:00
50c438e656 awk internalized 2025-05-28 14:14:58 +02:00
a72c2c73d2 awk based, fast 2025-05-28 14:02:20 +02:00
23fdb4c3d2 awk based 2025-05-27 21:24:21 +02:00
9545410139 updated readme 2025-05-23 17:01:03 +02:00
c2342b52a6 readme update 2025-05-23 16:58:27 +02:00
7aa85ce5af impreved fzf 2025-05-23 15:55:14 +02:00
5b292f6971 improved keys 2025-05-23 15:47:33 +02:00
74a75f1b4b more flags 2025-05-23 15:43:06 +02:00
ee8e6994f3 one temp file less 2025-05-23 14:57:03 +02:00
2df8fcf1bc improved view, fixes 2025-05-23 14:46:56 +02:00
9a5aaea387 Tons of changes 2025-05-23 14:04:18 +02:00
5bbde62142 fix:remove temporary files 2025-05-22 13:30:46 +02:00
10 changed files with 1227 additions and 347 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
fzf-vjour

View File

@@ -1,12 +1,11 @@
A [fzf](https://github.com/junegunn/fzf)-based **journaling and notes** application with CalDav support. A [fzf](https://github.com/junegunn/fzf)-based **journaling, notes, and tasks** application with CalDav support.
Description and Use Case Description and Use Case
------------------------ ------------------------
This application allows for a keyboard-controlled maneuvering of your notes and journal entries. This application allows for a keyboard-controlled maneuvering of your notes, journal entries, and tasks.
A journal entry is nothing more than a note associated with a specific date. These entries are stored as [iCalendar](https://datatracker.ietf.org/doc/html/rfc5545) files of the type `VJOURNAL` and `VTODO`.
These entries are stored as [iCalendar](https://datatracker.ietf.org/doc/html/rfc5545) files of the type `VJOURNAL`.
For instance, you could use this application as a minimalistic terminal-based counterpart of [jtx Board](https://jtx.techbee.at/) in a setup For instance, you could use this application as a terminal-based counterpart of [jtx Board](https://jtx.techbee.at/) in a setup
with a CalDav server, such as [Radicale](https://radicale.org/), and a synchronization tool like [vdirsyncer](http://vdirsyncer.pimutils.org/). with a CalDav server, such as [Radicale](https://radicale.org/), and a synchronization tool like [vdirsyncer](http://vdirsyncer.pimutils.org/).
Installation Installation
@@ -14,36 +13,31 @@ Installation
Just copy the file to your preferred location, e.g., `~/.local/bin`, and make it executable. Just copy the file to your preferred location, e.g., `~/.local/bin`, and make it executable.
### Requirements ### Requirements
This is a POSIX script with inline `python3` elements. This is a POSIX script with inline `awk` elements.
Make sure you have [fzf](https://github.com/junegunn/fzf), [batcat](https://github.com/sharkdp/bat), and [yq](https://github.com/mikefarah/yq) installed. Make sure you have [fzf](https://github.com/junegunn/fzf) and [batcat](https://github.com/sharkdp/bat).
For the `python3` code, we also require [icalendar](https://pypi.org/project/icalendar/).
Configuration Configuration
-------------- --------------
This application is configured with a YAML file located at `$HOME/.config/fzf-vjour/config.yaml`. This application is configured with a file located at `$HOME/.config/fzf-vjour/config`.
The entry `datadir` specifies the root directory of your journal and note entries. The entry `ROOT` specifies the root directory of your journal and note entries.
This directory may contain several subfolders, called _collections_. This directory may contain several subfolders, called _collections_.
The entry `collections` is a list, where each item specifies a subfolder, given by `name`, and a label, given by `label` (any string). 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. 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. 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. Finally, a third entry `SYNC_CMD` specifies the command to be executed for synchronizing.
Consider the following example: Consider the following example:
```yaml ```sh
datadir: ~/.journal ROOT=~/.journal/
sync_cmd: vdirsyncer sync journals COLLECTION_LABELS="745ae7a0-d723-4cd8-80c4-75f52f5b7d90=shared 👫🏼;12cacb18-d3e1-4ad4-a1d0-e5b209012e85=work 💼;"
collections: SYNC_CMD="vdirsyncer sync journals"
- name: 12cacb18-d3e1-4ad4-a1d0-e5b209012e85
label: 💼
- name: 745ae7a0-d723-4cd8-80c4-75f52f5b7d90
label: 🏡
``` ```
Here the files are stored in Here the files are stored in
`~/.journal/12cacb18-d3e1-4ad4-a1d0-e5b209012e85` (work-related entries) `~/.journal/12cacb18-d3e1-4ad4-a1d0-e5b209012e85` (work-related entries)
and and
`~/.journal/745ae7a0-d723-4cd8-80c4-75f52f5b7d90` (personal collection). `~/.journal/745ae7a0-d723-4cd8-80c4-75f52f5b7d90` (shared collection).
This configuration will work well with a `vdirsyncer` configuration such as This configuration will work well with a `vdirsyncer` configuration such as
```confini ```confini
@@ -59,20 +53,30 @@ path = "~/.journal"
[storage remote] [storage remote]
type = "caldav" type = "caldav"
item_types = ["VJOURNAL"] item_types = ["VJOURNAL", "VTODO"]
... ...
``` ```
Usage Usage
----- -----
Use the default `fzf` keys to navigate your notes. In addition, there are the following keybindings: Use the default `fzf` keys to navigate your notes, e.g., `ctrl-j` and `ctrl-k` for going down/up in the list.
In addition, there are the following keybindings:
| Key | Action | | Key | Action |
| --- | ------ | | --- | ------ |
| enter | Open note/journal in your `$EDITOR` | | `enter` | Open note/journal/task in your `$EDITOR` |
| ctrl-d | Delete the seleted entry | | `ctrl-alt-d` | Delete the seleted entry |
| ctrl-n | Make a new entry | | `ctrl-n` | Make a new entry |
| ctrl-r | Refresh the view | | `ctrl-r` | Refresh the view |
| ctrl-s | Run the synchronization command | | `ctrl-s` | Run the synchronization command |
| `ctrl-x` | Toggle task completion |
| `alt-up` | Increase task priority |
| `alt-down` | Decrease task priority |
| `alt-0` | Default view: Journal, notes, and _open_ tasks |
| `alt-1` | Display journal entries |
| `alt-2` | Display notes |
| `alt-3` | Display all tasks |
You may also invoke the script with `--help` to see further command-line options.
License License
------- -------

1008
fzf-vjour

File diff suppressed because it is too large Load Diff

11
scripts/build.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
BOLD="\033[1m"
GREEN="\033[0;32m"
OFF="\033[m"
NAME="fzf-vjour"
SRC="./src/fzf-vjour"
echo "🐔 ${GREEN}Building${OFF} ${BOLD}$NAME${OFF}"
sed -E 's|@@include (.+)$|cat \1|e' "$SRC" >"$NAME"
chmod +x "$NAME"
echo "🥚 ${GREEN}Done${OFF}"

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

@@ -0,0 +1,41 @@
# Increase/decrease priority, or toggle completed status
#
# If `delta` is specified using `-v`, then the priority value is increased by
# `delta.` If `delta` is unspecified (or equal to 0), then the completeness
# status is toggled.
BEGIN {
FS=":";
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1);
delta = delta + 0; # cast as integer
}
/^END:VTODO/ && inside {
# Print sequence and last-modified, if not yet printed
if (!seq) print "SEQUENCE:1";
if (!lm) print "LAST-MODIFIED:" zulu;
# Print priority
prio = prio ? prio + delta : 0 + delta;
prio = prio < 0 ? 0 : prio;
prio = prio > 9 ? 9 : prio;
print "PRIORITY:" prio;
# Print status (toggle if needed)
bit_status = status == "COMPLETED" ? 1 : 0;
bit_toggle = delta ? 0 : 1;
percent = xor(bit_status, bit_toggle) ? 100 : 0;
status = xor(bit_status, bit_toggle) ? "COMPLETED" : "NEEDS-ACTION";
print "STATUS:" status
print "PERCENT-COMPLETE:" percent
# print rest
inside = "";
print $0;
next
}
/^BEGIN:VTODO/ { inside = 1; print; next }
/^SEQUENCE/ && inside { seq = 1; print "SEQUENCE:" $2+1; next }
/^LAST-MODIFIED/ && inside { lm = 1; print "LAST-MODIFIED:" zulu; next }
/^PRIORITY:/ && inside { prio = $2; next }
/^STATUS/ && inside { status = $2; next }
/^PERCENT-COMPLETE/ && inside { next } # ignore, we take STATUS:COMPLETED as reference
{ print }

39
src/awk/export.awk Normal file
View File

@@ -0,0 +1,39 @@
function getcontent(content_line, prop)
{
return substr(content_line[prop], index(content_line[prop], ":") + 1);
}
function storetext_line(content_line, c, prop)
{
c[prop] = getcontent(content_line, prop);
gsub("\\\\n", "\n", c[prop]);
gsub("\\\\N", "\n", c[prop]);
gsub("\\\\,", ",", c[prop]);
gsub("\\\\;", ";", c[prop]);
gsub("\\\\\\\\", "\\", c[prop]);
}
BEGIN { FS = "[:;]"; }
/^BEGIN:(VJOURNAL|VTODO)/ { type = $2 }
/^END:/ && $2 == type { exit }
/^(CATEGORIES|DESCRIPTION|SUMMARY|DUE)/ { prop = $1; content_line[prop] = $0; next; }
/^[^ ]/ && prop { prop = ""; next; }
/^ / && prop { content_line[prop] = content_line[prop] substr($0, 2); next; }
END {
if (!type) {
exit
}
# Process content lines
storetext_line(content_line, c, "CATEGORIES" );
storetext_line(content_line, c, "DESCRIPTION");
storetext_line(content_line, c, "SUMMARY" );
storetext_line(content_line, c, "DUE" );
# Print
if (c["DUE"])
print "::: <| " substr(c["DUE"], 1, 4) "-" substr(c["DUE"], 5, 2) "-" substr(c["DUE"], 7, 2);
print "# " c["SUMMARY"];
print "> " c["CATEGORIES"];
print "";
print c["DESCRIPTION"];
}

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

@@ -0,0 +1,18 @@
# print content of field `field`
BEGIN { FS = ":"; regex = "^" field; }
/^BEGIN:(VJOURNAL|VTODO)/ { type = $2 }
/^END:/ && $2 == type { exit }
$0 ~ field { content = $0; next; }
/^ / && content { content = content substr($0, 2); next; }
/^[^ ]/ && content { exit }
END {
if (!type) { exit }
# Process content line
content = substr(content, index(content, ":") + 1);
gsub("\\\\n", "\n", content);
gsub("\\\\N", "\n", content);
gsub("\\\\,", ",", content);
gsub("\\\\;", ";", content);
gsub("\\\\\\\\", "\\", content);
print content;
}

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

@@ -0,0 +1,215 @@
# awk script to generate summary line for iCalendar VJOURNAL and VTODO entries
#
# See https://datatracker.ietf.org/doc/html/rfc5545 for the RFC 5545 that
# describes iCalendar, and its syntax
function getcontent(content_line, prop)
{
return substr(content_line[prop], index(content_line[prop], ":") + 1);
}
function storetext_line(content_line, c, prop)
{
c[prop] = getcontent(content_line, prop);
gsub("\\\\n", " ", c[prop]);
gsub("\\\\N", " ", c[prop]);
gsub("\\\\,", ",", c[prop]);
gsub("\\\\;", ";", c[prop]);
gsub("\\\\\\\\", "\\", c[prop]);
#gsub(" ", "_", c[prop]);
}
function storeinteger(content_line, c, prop)
{
c[prop] = getcontent(content_line, prop);
c[prop] = c[prop] ? c[prop] : 0;
}
function storedatetime(content_line, c, prop)
{
c[prop] = getcontent(content_line, prop);
}
function storedate(content_line, c, prop)
{
c[prop] = substr(getcontent(content_line, prop), 1, 8);
}
function formatdate(date, today, todaystamp, ts, ts_y, ts_m, ts_d, delta)
{
ts_y = substr(date, 1, 4);
ts_m = substr(date, 5, 2);
ts_d = substr(date, 7);
ts = mktime(ts_y " " ts_m " " ts_d " 00 00 00");
delta = (ts - todaystamp) / 86400;
if (delta >= 0 && delta < 1) {
return " today";
}
if (delta >= 1 && delta < 2) {
return " tomorrow";
}
if (delta >= 2 && delta < 3) {
return " in two days";
}
if (delta >= 3 && delta < 4) {
return " in three days";
}
if (delta < 0 && delta >= -1) {
return " yesterday";
}
if (delta < -1 && delta >= -2) {
return " two days ago";
}
if (delta < -2 && delta >= -3) {
return "three days ago";
}
return " " substr(date, 1, 4) "-" substr(date, 5, 2) "-" substr(date, 7);
}
BEGIN {
# We require the following variables to be set using -v
# collection_lables: ;-delimited collection=label strings
# flag_open: symbol for open to-dos
# flag_completed: symbol for completed to-dos
# flag_journal: symbol for journal entries
# flag_note: symbol for note entries
FS = "[:;]";
# Collections
split(collection_labels, mapping, ";");
for (map in mapping)
{
split(mapping[map], m, "=");
collection2label[m[1]] = m[2];
}
# Colors
GREEN = "\033[1;32m";
RED = "\033[1;31m";
WHITE = "\033[1;97m";
CYAN = "\033[1;36m";
FAINT = "\033[2m";
OFF = "\033[m";
# For date comparision
today = strftime("%Y%m%d");
todaystamp = mktime(substr(today, 1, 4) " " substr(today, 5, 2) " " substr(today, 7) " 00 00 00");
}
# Reset variables
BEGINFILE {
type = "";
prop = "";
delete content_line;
delete c;
}
/^BEGIN:(VJOURNAL|VTODO)/ {
type = $2
}
/^END:/ && $2 == type {
nextfile
}
/^(CATEGORIES|DESCRIPTION|PRIORITY|STATUS|SUMMARY|COMPLETED|DUE|DTSTART|DURATION|CREATED|DTSTAMP|LAST-MODIFIED)/ {
prop = $1;
content_line[prop] = $0;
next;
}
/^[^ ]/ && prop {
prop = "";
next;
}
/^ / && prop {
content_line[prop] = content_line[prop] substr($0, 2);
next;
}
ENDFILE {
if (!type) {
exit
}
# Process content lines
storetext_line(content_line, c, "CATEGORIES" );
storetext_line(content_line, c, "DESCRIPTION" );
storeinteger( content_line, c, "PRIORITY" );
storetext_line(content_line, c, "STATUS" );
storetext_line(content_line, c, "SUMMARY" );
storedatetime( content_line, c, "COMPLETED" );
storedate( content_line, c, "DUE" );
storedate( content_line, c, "DTSTART" );
storedatetime( content_line, c, "DURATION" );
storedatetime( content_line, c, "CREATED" );
storedatetime( content_line, c, "DTSTAMP" );
storedatetime( content_line, c, "LAST-MODIFIED");
# Priority field, primarly used for sorting
priotext = "";
prio = 0;
if (c["PRIORITY"] > 0)
{
priotext = "❗(" c["PRIORITY"] ") ";
prio = 10 - c["PRIORITY"];
}
# Last modification/creation time stamp, used for sorting
# LAST-MODIFIED: Optional field for VTODO and VJOURNAL entries, date-time in
# UTC time format
# DTSTAMP: mandatory field in VTODO and VJOURNAL, date-time in UTC time
# format
mod = c["LAST-MODIFIED"] ? c["LAST-MODIFIED"] : c["DTSTAMP"];
# Collection name
depth = split(FILENAME, path, "/");
collection = depth > 1 ? path[depth-1] : "";
collection = collection in collection2label ? collection2label[collection] : collection;
# Date field. For VTODO entries, we show the due date, for journal entries,
# the associated date.
datecolor = CYAN;
summarycolor = GREEN;
if (type == "VTODO")
{
# Either DUE or DURATION may appear. If DURATION appears, then also DTSTART
d = c["DUE"] ? c["DUE"] :
(c["DURATION"] ? c["DTSTART"] " for " c["DURATION"] : "");
if (d && d <= today && c["STATUS"] != "COMPLETED")
{
datecolor = RED;
summarycolor = RED;
}
} else {
d = c["DTSTART"];
}
d = d ? formatdate(d, today, todaystamp ts, ts_y, ts_m, ts_d, delta) : " ";
# flag: - "journal" for VJOURNAL with DTSTART
# - "note" for VJOURNAL without DTSTART
# - "completed" for VTODO with c["STATUS"] == COMPLETED
# - "open" for VTODO with c["STATUS"] != COMPLETED
if (type == "VTODO")
flag = c["STATUS"] == "COMPLETED" ? flag_completed : flag_open;
else
flag = c["DTSTART"] ? flag_journal : flag_note;
# summary
# c["SUMMARY"]
summary = c["SUMMARY"] ? c["SUMMARY"] : " "
# categories
categories = c["CATEGORIES"] ? c["CATEGORIES"] : " "
# filename
# FILENAME
print prio,
mod,
collection,
datecolor d OFF,
flag,
priotext summarycolor summary OFF,
WHITE categories OFF,
" " FAINT FILENAME OFF;
}

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

@@ -0,0 +1,96 @@
function escape_categories(str)
{
gsub("\\\\", "\\\\", str);
gsub(";", "\\\\;", str);
}
function escape(str)
{
escape_categories(str)
gsub(",", "\\\\,", str);
}
function print_fold(nameparam, content, i, s)
{
i = 74 - length(nameparam);
s = substr(content, 1, i);
print nameparam s;
s = substr(content, i+1, 73);
i = i + 73;
while (s)
{
print " " s;
s = substr(content, i+1, 73);
i = i + 73;
}
}
BEGIN {
FS=":";
type = "VJOURNAL";
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1);
}
desc { desc = desc "\\n" $0; next; }
{
if (substr($0, 1, 6) == "::: |>")
{
start = substr(zulu, 1, 8);
getline;
}
if (substr($0, 1, 6) == "::: <|")
{
type = "VTODO"
due = substr($0, 8);
getline;
}
summary = substr($0, 1, 2) != "# " ? "" : substr($0, 3);
getline;
categories = substr($0, 1, 1) != ">" ? "" : substr($0, 3);
getline; # This line should be empty
getline; # First line of description
desc = $0;
next;
}
END {
# Sanitize input
if (due) {
# Use command line `date` for parsing
cmd = "date -d \"" due "\" +\"%Y%m%d\"";
cmd | getline res
due = res ? res : ""
}
escape(summary);
escape(desc);
escape_categories(categories);
# print ical
print "BEGIN:VCALENDAR";
print "VERSION:2.0";
print "CALSCALE:GREGORIAN";
print "PRODID:-//fab//awk//EN";
print "BEGIN:" type;
print "DTSTAMP:" zulu;
print "UID:" uid;
print "CLASS:PRIVATE";
print "CREATED:" zulu;
print "SEQUENCE:1";
print "LAST-MODIFIED:" zulu;
if (type == "VTODO")
{
print "STATUS:NEEDS-ACTION";
print "PERCENT-COMPLETE:0";
if (due)
print "DUE;VALUE=DATE:" due;
}
else
{
print "STATUS:FINAL";
if (start)
print "DTSTART;VALUE=DATE:" start;
}
if (summary) print_fold("SUMMARY:", summary, i, s);
if (categories) print_fold("CATEGORIES:", categories, i, s);
if (desc) print_fold("DESCRIPTION:", desc, i, s);
print "END:" type;
print "END:VCALENDAR"
}

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

@@ -0,0 +1,85 @@
function getcontent(content_line, prop)
{
return substr(content_line[prop], index(content_line[prop], ":") + 1);
}
function escape_categories(str)
{
gsub("\\\\", "\\\\", str);
gsub(";", "\\\\;", str);
}
function escape(str)
{
escape_categories(str)
gsub(",", "\\\\,", str);
}
function print_fold(nameparam, content, i, s)
{
i = 74 - length(nameparam);
s = substr(content, 1, i);
print nameparam s;
s = substr(content, i+1, 73);
i = i + 73;
while (s)
{
print " " s;
s = substr(content, i+1, 73);
i = i + 73;
}
}
BEGIN {
FS=":";
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1);
}
ENDFILE {
if (NR == FNR)
{
# Sanitize input
if (due) {
# Use command line `date` for parsing
cmd = "date -d \"" due "\" +\"%Y%m%d\"";
cmd | getline res
due = res ? res : ""
}
escape(summary);
escape(desc);
escape_categories(categories);
}
}
NR == FNR && desc { desc = desc "\\n" $0; next; }
NR == FNR {
if (substr($0, 1, 6) == "::: <|")
{
due = substr($0, 8);
getline;
}
summary = substr($0, 1, 2) != "# " ? "" : substr($0, 3);
getline;
categories = substr($0, 1, 1) != ">" ? "" : substr($0, 3);
getline; # This line should be empty
getline; # First line of description
desc = $0;
next;
}
/^BEGIN:(VJOURNAL|VTODO)/ { type = $2; print; next }
/^X-ALT-DESC/ && type { next } # drop this alternative description
/^ / && type { next } # drop this folded line (the only content with folded lines will be updated)
/^(DUE|SUMMARY|CATEGORIES|DESCRIPTION|LAST-MODIFIED)/ && type { next } # skip for now, we will write updated fields at the end
/^SEQUENCE/ && type { seq = $2; next } # store sequence number and skip
/^END:/ && type == $2 {
seq = seq ? seq + 1 : 1;
print "SEQUENCE:" seq;
print "LAST-MODIFIED:" zulu;
if (due) print "DUE;VALUE=DATE:" due;
print_fold("SUMMARY:", summary, i, s);
print_fold("CATEGORIES:", categories, i, s);
print_fold("DESCRIPTION:", desc, i, s);
type = "";
}
{ print }