9 Commits
v0.1 ... local

Author SHA1 Message Date
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
2 changed files with 482 additions and 183 deletions

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
@@ -15,7 +14,7 @@ Just copy the file to your preferred location, e.g., `~/.local/bin`, and make it
### Requirements ### Requirements
This is a POSIX script with inline `python3` elements. This is a POSIX script with inline `python3` 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), [batcat](https://github.com/sharkdp/bat), [jq](https://jqlang.org/), and [yq](https://github.com/mikefarah/yq) installed.
For the `python3` code, we also require [icalendar](https://pypi.org/project/icalendar/). For the `python3` code, we also require [icalendar](https://pypi.org/project/icalendar/).
Configuration Configuration
@@ -23,7 +22,7 @@ Configuration
This application is configured with a YAML file located at `$HOME/.config/fzf-vjour/config.yaml`. This application is configured with a YAML file located at `$HOME/.config/fzf-vjour/config.yaml`.
The entry `datadir` specifies the root directory of your journal and note entries. The entry `datadir` 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 `collections` is a list, where each item specifies a subfolder, given by `name`, and a label, given by `label` (any string free of white spaces).
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.
@@ -34,9 +33,9 @@ datadir: ~/.journal
sync_cmd: vdirsyncer sync journals sync_cmd: vdirsyncer sync journals
collections: collections:
- name: 12cacb18-d3e1-4ad4-a1d0-e5b209012e85 - name: 12cacb18-d3e1-4ad4-a1d0-e5b209012e85
label: 💼 label: work:💼
- name: 745ae7a0-d723-4cd8-80c4-75f52f5b7d90 - name: 745ae7a0-d723-4cd8-80c4-75f52f5b7d90
label: 🏡 label: priv:🏡
``` ```
@@ -59,20 +58,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-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
------- -------

680
fzf-vjour
View File

@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
set -eu set -u
# Read configuration # Read configuration
CONFIGFILE="$HOME/.config/fzf-vjour/config.yaml" CONFIGFILE="$HOME/.config/fzf-vjour/config.yaml"
@@ -24,71 +24,58 @@ SED_COLLECTIONLABELS_TO_NAMES=$(
yq '.collections[] | "s|\ *" + .label + "\ *|/" + .name + "/|"' <"$CONFIGFILE" | yq '.collections[] | "s|\ *" + .label + "\ *|/" + .name + "/|"' <"$CONFIGFILE" |
xargs printf "-e \"%s\" " xargs printf "-e \"%s\" "
) )
COLLECTION_LABEL_MAX_LEN=$(yq '[.collections[].label | length] | max' <"$CONFIGFILE") COLLECTION_NAME_MAX_LEN=$(yq '[.collections[].name | length] | max' <"$CONFIGFILE")
LABLES=$(yq '.collections[].label' <"$CONFIGFILE") LABLES=$(yq '.collections[].label' <"$CONFIGFILE")
SYNC_CMD=$(yq '.sync_cmd' <"$CONFIGFILE") SYNC_CMD=$(yq '.sync_cmd' <"$CONFIGFILE")
LONGSPACE=" "
__vjournalnew() { if ! (yq '.collections[].label' | grep ' ') >/dev/null; then
python3 -c ' echo "We currently do not support whitespaces in the labels"
import sys exit 1
from datetime import date, datetime fi
from icalendar.cal import Calendar, Journal
if not len(sys.argv) == 2: __vtodopriority() {
print("Pass uid as first argument!", file=sys.stderr)
sys.exit(1)
UID = sys.argv[1]
ical = Calendar()
j = Journal()
isnote = True
line = sys.stdin.readline().strip()
if line[:4] == "::: ":
isnote = False
line = sys.stdin.readline().strip()
if not line[:2] == "# ":
print("Error: Summary line is corrupt!", file=sys.stderr)
sys.exit(1)
summary = line[2:]
line = sys.stdin.readline().strip()
if not line[:2] == "> " and not line == ">":
print("Error: Categories line is corrupt!", file=sys.stderr)
sys.exit(1)
categories = line[2:].split(",")
line = sys.stdin.readline().strip()
if not line == "":
print("Error: Missing separating line!", file=sys.stderr)
sys.exit(1)
description = sys.stdin.read()
# Create ical
j["SUMMARY"] = summary
j.categories = categories
j["DESCRIPTION"] = description
j.LAST_MODIFIED = datetime.utcnow()
j.DTSTAMP = datetime.utcnow()
j["UID"] = UID
j["SEQUENCE"] = 1
j["STATUS"] = "FINAL"
if not isnote:
j.DTSTART = date.today()
ical.add_component(j)
ical["VERSION"] = "2.0"
ical["PRODID"] = "vjournew/basic"
# Print
print(ical.to_ical().decode().replace("\r\n", "\n"))' "$@"
}
__vjournalupdate() {
python3 -c ' python3 -c '
import sys import sys
from datetime import datetime from datetime import datetime
from icalendar.cal import Journal from icalendar.cal import Todo
if not len(sys.argv) == 3:
print("Pass ical file as first argument!", file=sys.stderr)
sys.exit(1)
increase = 1 if sys.argv[2] == "1" else -1
with open(sys.argv[1], "r") as f:
try:
ical = Todo.from_ical(f.read())
except Exception as e:
print(f"Failed to read vjournal file: {e}", file=sys.stderr)
sys.exit(1)
tlist = [component for component in ical.walk("VTODO")]
if len(tlist) == 0:
print("ical file is not a VTODO", file=sys.stderr)
sys.exit(1)
t = tlist[0]
# Update ical
priority = t.pop("PRIORITY")
priority = (int(priority) if priority else 0) + increase
priority = 0 if priority < 0 else 9 if priority > 9 else priority
t["PRIORITY"] = priority
# Print
print(ical.to_ical().decode().replace("\r\n", "\n"))
' "$@"
}
__vtodotogglecompleted() {
python3 -c '
import sys
from datetime import datetime
from icalendar.cal import Todo
from icalendar.prop import vDDDTypes
if not len(sys.argv) == 2: if not len(sys.argv) == 2:
print("Pass ical file as first argument!", file=sys.stderr) print("Pass ical file as first argument!", file=sys.stderr)
@@ -96,18 +83,64 @@ if not len(sys.argv) == 2:
with open(sys.argv[1], "r") as f: with open(sys.argv[1], "r") as f:
try: try:
ical = Journal.from_ical(f.read()) ical = Todo.from_ical(f.read())
except Exception as e: except Exception as e:
print(f"Failed to read vjournal file: {e}", file=sys.stderr) print(f"Failed to read vjournal file: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
jlist = [component for component in ical.walk("VJOURNAL")] tlist = [component for component in ical.walk("VTODO")]
if len(jlist) == 0: if len(tlist) == 0:
print("ical file is not a VJOURNAL", file=sys.stderr) print("ical file is not a VTODO", file=sys.stderr)
sys.exit(1) sys.exit(1)
j = jlist[0] t = tlist[0]
# Update ical
if t.has_key("STATUS") and t["STATUS"] == "COMPLETED":
# Mark as not completed
t["STATUS"] = "NEEDS-ACTION"
t["PERCENT-COMPLETE"] = 0
if t.has_key("COMPLETED"): t.pop("COMPLETED")
else:
t["STATUS"] = "COMPLETED"
t["PERCENT-COMPLETE"] = 100
t["COMPLETED"] = vDDDTypes(datetime.utcnow())
# Print
print(ical.to_ical().decode().replace("\r\n", "\n"))
' "$@"
}
__vicalnew() {
python3 -c '
import sys
from datetime import date, datetime
from icalendar.cal import Calendar, Journal, Todo
from icalendar.prop import vDDDTypes
if not len(sys.argv) == 2:
print("Pass UID as first argument!", file=sys.stderr)
sys.exit(1)
UID = sys.argv[1]
start = None
due = None
line = sys.stdin.readline().strip() line = sys.stdin.readline().strip()
if line[:6] == "::: |>":
start = datetime.utcnow().date()
line = sys.stdin.readline().strip()
if line[:6] == "::: <|":
lst = line.split(" ")
due = True
if len(lst) >= 3:
try:
duedate = datetime.strptime(lst[2], "%Y-%m-%d").date()
due = duedate
except Exception as e:
pass
line = sys.stdin.readline().strip()
if not line[:2] == "# ": if not line[:2] == "# ":
print("Error: Summary line is corrupt!", file=sys.stderr) print("Error: Summary line is corrupt!", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -115,11 +148,142 @@ summary = line[2:]
line = sys.stdin.readline().strip() line = sys.stdin.readline().strip()
if not line[:2] == "> " and not line == ">": if not line[:2] == "> " and not line == ">":
print("Error: Categories line is corrupt!", file=sys.stderr) categories = []
sys.exit(1) else:
categories = line[2:].split(",") categories = line[2:].split(",")
line = sys.stdin.readline().strip()
if not line == "":
print("Error: Missing separating line!", file=sys.stderr)
sys.exit(1)
description = sys.stdin.read()
# Create ical
now = datetime.utcnow()
ical = Calendar()
if due:
o = Todo()
# The following are REQUIRED, but MUST NOT occur more than once.
# dtstamp / uid /
o.DTSTAMP = now
o["UID"] = UID
# The following are OPTIONAL, but MUST NOT occur more than once.
# class / completed / created / description / dtstart / geo / last-mod /
# location / organizer / percent / priority / recurid / seq / status /
# summary / url /
o["CLASS"] = "PRIVATE"
o["CREATED"] = vDDDTypes(now)
o["DESCRIPTION"] = description.strip()
o.LAST_MODIFIED = now
o["PRIORITY"] = 0
o["SEQUENCE"] = 0
o["STATUS"] = "NEEDS-ACTION"
o["SUMMARY"] = summary
# The following is OPTIONAL, but SHOULD NOT occur more than once.
# rrule /
# Either "due" or "duration" MAY appear in a "todoprop", but "due" and
# "duration" MUST NOT occur in the same "todoprop". If "duration" appear in
# a "todoprop", then "dtstart" MUST also appear in the same "todoprop".
# due / duration /
if isinstance(due, date):
o.DUE = due
# The following are OPTIONAL, and MAY occur more than once.
# attach / attendee / categories / comment / contact / exdate / rstatus /
# related / resources / rdate / x-prop / iana-prop
o.categories = categories
else:
o = Journal()
# The following are REQUIRED, but MUST NOT occur more than once.
# dtstamp / uid /
o.DTSTAMP = now
o["UID"] = UID
# The following are OPTIONAL, but MUST NOT occur more than once.
# class / created / dtstart / last-mod / organizer / recurid / seq / status
# / summary / url /
o["CLASS"] = "PRIVATE"
o["CREATED"] = vDDDTypes(now)
o.DTSTART = start
o.LAST_MODIFIED = now
o["SEQUENCE"] = 0
o["STATUS"] = "FINAL"
o["SUMMARY"] = summary
# The following is OPTIONAL, but SHOULD NOT occur more than once.
# rrule /
# The following are OPTIONAL, and MAY occur more than once.
# attach / attendee / categories / comment / contact / description / exdate
# / related / rdate / rstatus / x-prop / iana-prop
o.categories = categories
o["DESCRIPTION"] = description.strip()
ical.add_component(o)
ical["PRODID"] = "fzf-vjour/basic"
ical["VERSION"] = "2.0"
# Print
print(ical.to_ical().decode().replace("\r\n", "\n"))' "$@"
}
__icalupdate() {
python3 -c '
import sys
from datetime import date, datetime
from icalendar.cal import Calendar, Todo
if not len(sys.argv) == 2:
print("Pass ical file as first argument!", file=sys.stderr)
sys.exit(1)
with open(sys.argv[1], "r") as f:
try:
ical = Calendar.from_ical(f.read())
except Exception as e:
print(f"Failed to read ical file: {e}", file=sys.stderr)
sys.exit(1)
olist = [component for component in ical.walk(select=lambda c:c.name in ["VJOURNAL", "VTODO"])]
if len(olist) == 0:
sys.exit(0)
o = olist[0]
line = sys.stdin.readline().strip() line = sys.stdin.readline().strip()
due = None
if isinstance(o, Todo):
if not line[:6] == "::: <|":
print("Error: Due date line is corrupt!", file=sys.stderr)
sys.exit(1)
lst = line.split(" ")
due = True
if len(lst) >= 3:
try:
duedate = datetime.strptime(lst[2], "%Y-%m-%d").date()
due = duedate
except Exception as e:
pass
line = sys.stdin.readline().strip()
if not line[:2] == "# ":
print("Error: Summary line is corrupt!", file=sys.stderr)
sys.exit(1)
summary = line[2:]
line = sys.stdin.readline().strip()
if not line[:2] == "> " and not line == ">":
categories = []
else:
categories = line[2:].split(",")
line = sys.stdin.readline().strip()
if not line == "": if not line == "":
print("Error: Missing separating line!", file=sys.stderr) print("Error: Missing separating line!", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -127,100 +291,77 @@ if not line == "":
description = sys.stdin.read() description = sys.stdin.read()
# Update ical # Update ical
j["SUMMARY"] = summary if due:
j.categories = categories if isinstance(due, date):
j["DESCRIPTION"] = description o.DUE = due
j.LAST_MODIFIED = datetime.utcnow() elif "DUE" in o.keys():
o.pop("DUE")
o["SUMMARY"] = summary
o.categories = categories
o["DESCRIPTION"] = description.strip()
o.LAST_MODIFIED = datetime.utcnow()
seq = o["SEQUENCE"] if "SEQUENCE" in o.keys() else 0
o["SEQUENCE"] = seq + 1
# Print # Print
print(ical.to_ical().decode().replace("\r\n", "\n")) print(ical.to_ical().decode().replace("\r\n", "\n"))
' "$@" ' "$@"
} }
__vjournal2json() { __ical2json() {
python3 -c ' python3 -c '
import sys import sys
import json import json
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from icalendar.cal import Journal from icalendar.cal import Calendar, Todo
input_data = sys.stdin.read() input_data = sys.stdin.read()
try: try:
ical = Journal.from_ical(input_data) ical = Calendar.from_ical(input_data)
except Exception as e: except Exception as e:
print(f"Failed to read vjournal file: {e}", file=sys.stderr) print(f"Failed to read ical file: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
jlist = [component for component in ical.walk("VJOURNAL")] olist = [component for component in ical.walk(select=lambda c:c.name in ["VJOURNAL", "VTODO"])]
if len(jlist) == 0: if len(olist) == 0:
sys.exit(0) sys.exit(0)
j = jlist[0] o = olist[0]
local_tz = ZoneInfo("localtime") local_tz = ZoneInfo("localtime")
data = { data = {
"summary": j.get("SUMMARY"), "summary": o.get("SUMMARY"),
"description": j.get("DESCRIPTION"), "description": o.get("DESCRIPTION") if "DESCRIPTION" in o.keys() else "",
"categories": j.categories, "categories": o.categories,
"class": j.get("CLASS"), "class": o.get("CLASS"),
"created": str(j.DTSTAMP.astimezone(local_tz)), "created": str(o.DTSTAMP.astimezone(local_tz)) if o.DTSTAMP else "",
"last_modified": str(j.LAST_MODIFIED.astimezone(local_tz)), "last_modified": str(o.LAST_MODIFIED.astimezone(local_tz)) if o.LAST_MODIFIED else "",
"start": str( "start": str(
j.DTSTART.astimezone(local_tz) o.DTSTART.astimezone(local_tz)
if isinstance(j.DTSTART, datetime) if isinstance(o.DTSTART, datetime)
else j.DTSTART or "" else o.DTSTART or ""
), ),
} }
if isinstance(o, Todo):
data["due"] = str(o.DUE) if o.DUE else ""
print(json.dumps(data))' print(json.dumps(data))'
} }
# Process each file
# This function takes two arguments:
#
# @param string: Path to ics file
# @param string: Maximum length of filenames (for padding purposes)
__filepath_to_searchline() {
filepath="$1"
maxlen="$2"
totallen=$((maxlen + ${#ROOT}))
filepathpad=$(printf "%-${totallen}s" "$filepath")
# Color support __date_not_in_future() {
GREEN=$(printf '\033[1;32m') date_target=$(date -d "$1" +%s)
WHITE=$(printf '\033[1;97m') date_today=$(date -d "00:00" +%s)
FAINT=$(printf '\033[2m') date_delta=$((date_target - date_today))
OFF=$(printf '\033[m') if [ "$date_delta" -le 0 ]; then
echo 1
# Parse file
summary=""
categories=""
dtstamp=""
dtstart=""
while IFS= read -r line; do
case "$line" in
SUMMARY:*)
summary="${line#SUMMARY:}"
;;
CATEGORIES:*)
categories="${line#CATEGORIES:}"
;;
DTSTAMP:*)
dtstamp="${line#DTSTAMP:}"
;;
DTSTART*:[0-9]*)
dtstart=$(echo "$line" | grep -oE '[0-9]{8}')
;;
esac
if [ -n "$summary" ] && [ -n "$categories" ] && [ -n "$dtstamp" ] && [ -n "$dtstart" ]; then
break
fi fi
done <"$filepath" }
# Parse date __date_to_expression() {
if [ -n "$dtstart" ]; then date_target=$(date -d "$1" +%s)
date_target=$(date -d "$dtstart" +%s) date_today=$(date -d "00:00" +%s)
date_today=$(date +%s)
date_delta=$(((date_target - date_today) / 86400)) date_delta=$(((date_target - date_today) / 86400))
date_expr=$date_delta date_expr=$date_delta
if [ "$date_delta" -eq 0 ]; then if [ "$date_delta" -eq 0 ]; then
@@ -230,39 +371,96 @@ __filepath_to_searchline() {
elif [ "$date_delta" -eq 1 ]; then elif [ "$date_delta" -eq 1 ]; then
date_expr="tomorrow" date_expr="tomorrow"
elif [ "$date_delta" -lt -1 ] && [ "$date_delta" -ge -7 ]; then elif [ "$date_delta" -lt -1 ] && [ "$date_delta" -ge -7 ]; then
date_expr="last $(date -d "$dtstart" +%A)" date_expr="last $(date -d "$1" +%A)"
elif [ "$date_delta" -gt 1 ] && [ "$date_delta" -le 7 ]; then elif [ "$date_delta" -gt 1 ] && [ "$date_delta" -le 7 ]; then
date_expr="next $(date -d "$dtstart" +%A)" date_expr="next $(date -d "$1" +%A)"
else else
date_expr=$(date -d "$dtstart" +%x) date_expr=$(date -d "$1" +%x)
fi fi
date_emoji="📘" echo "$date_expr"
}
# Process each file
# This function takes two arguments:
#
# @param string: Path to ics file
# @param string: Maximum length of filenames (for padding purposes)
__filepath_to_searchline() {
filepath="$1"
collection=$(dirname "$filepath" | sed "s|^$ROOT/*||")
filename=$(basename "$filepath")
# Color support
GREEN=$(printf '\033[1;32m')
RED=$(printf '\033[1;31m')
WHITE=$(printf '\033[1;97m')
FAINT=$(printf '\033[2m')
OFF=$(printf '\033[m')
# Parse file
summary=$(grep '^SUMMARY:' "$filepath" | cut -d ':' -f 2 | sed 's/\\,/,/g')
categories=$(grep '^CATEGORIES:' "$filepath" | cut -d ':' -f 2)
dtstamp=$(grep '^LAST-MODIFIED:' "$filepath" | cut -d ':' -f 2)
if [ -z "$dtstamp" ]; then
dtstamp=$(grep '^DTSTAMP:' "$filepath" | cut -d ':' -f 2)
fi
dtstart=$(grep '^DTSTART' "$filepath" | grep -oE '[0-9]{8}')
due=$(grep '^DUE' "$filepath" | grep -oE '[0-9]{8}')
priority=$(grep '^PRIORITY:' "$filepath" | cut -d ':' -f 2)
task=$(grep '^BEGIN:VTODO' "$filepath")
completed=$(grep '^STATUS:COMPLETED' "$filepath")
# Parse date
if [ -n "$dtstart" ]; then
emoji="📘"
date_expr=$(__date_to_expression "$dtstart")
else else
date_emoji="🗒️" emoji="🗒️"
date_expr="" date_expr=""
fi fi
# Check if this is a task
if [ -n "$task" ]; then
emoji="🔲"
if [ -n "$completed" ]; then
emoji="✅"
fi
fi
# Check Priority
if [ -n "$priority" ] && [ "$priority" -gt 0 ]; then
prioritymsg="❗($priority) "
priority=$((10 - priority))
else
prioritymsg=""
priority=0
fi
# Check due date
if [ -n "$due" ]; then
date_expr=$(__date_to_expression "$due")
fi
date_expr=$(printf "%12s" "$date_expr") date_expr=$(printf "%12s" "$date_expr")
# Color date
notinfuture=$(__date_not_in_future "$due")
if [ -n "$notinfuture" ] && [ -n "$due" ]; then
date_expr="$RED$date_expr$OFF"
summary_color="$RED"
else
date_expr="$WHITE$date_expr$OFF"
summary_color="$GREEN"
fi
# Print line # Print line
echo "$dtstamp $filepath $WHITE$date_expr$OFF $date_emoji $GREEN$summary$OFF $FAINT$categories$OFF" echo "$priority $dtstamp $collection $date_expr $emoji $prioritymsg$summary_color$summary$OFF $FAINT$categories$OFF$LONGSPACE/$filename"
#echo "$priority $dtstamp $filepathpad $date_expr $emoji $prioritymsg$summary_color$summary$OFF $FAINT$categories$OFF"
} }
__lines() { __lines() {
# Collect all files lines=$(find "$ROOT" -type f -name '*.ics' |
FILES_TMP=$(mktemp)
trap 'rm -f "$FILES_TMP"' EXIT
find "$ROOT" -type f -name '*.ics' >"$FILES_TMP"
# Compute max length of basenames
maxlen=0
while IFS= read -r file; do while IFS= read -r file; do
name=$(basename "$file") __filepath_to_searchline "$file"
[ ${#name} -gt "$maxlen" ] && maxlen=${#name} done)
done <"$FILES_TMP"
lines=$(while IFS= read -r file; do
__filepath_to_searchline "$file" "$maxlen"
done <"$FILES_TMP")
# Decorate # Decorate
lines=$(echo "$lines" | eval "$SED_COLLECTIONNAMES_TO_LABELS") lines=$(echo "$lines" | eval "$SED_COLLECTIONNAMES_TO_LABELS")
@@ -274,67 +472,156 @@ __lines() {
} }
__filepath_from_selection() { __filepath_from_selection() {
selection=$(echo "$1" | cut -d " " -f 1,2 | eval "$SED_COLLECTIONLABELS_TO_NAMES" | sed "s|^|$ROOT/|") filename=$(echo "$1" | rev | cut -d "/" -f 1 | rev)
echo "$selection" dirname=$(echo "$1" | cut -d " " -f 1 | eval "$SED_COLLECTIONLABELS_TO_NAMES" | sed "s|^|$ROOT|")
echo "$dirname/$filename"
} }
# Program starts here # Program starts here
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 " --tasks Show tasks only"
echo " --no-tasks Ignore tasks"
echo " --notes Show notes only"
echo " --no-notes Ignore notes"
echo " --journal Show journal only"
echo " --no-journal Ignore journal"
echo " --completed Show completed tasks only"
echo " --no-completed Ignore completed tasks"
echo " --new Create new entry"
echo ""
echo "The following options are for internal use."
echo " --reload Reload list"
echo " --preview <selection> Generate preview"
echo " --delete <selection> Delete selected entry"
echo " --decrease-priority <selection> Decrease priority of selected task"
echo " --increase-priority <selection> Increase priority of selected task"
echo " --toggle-completed <selection> Toggle completion flag of task"
exit
fi
# Command line arguments to be self-contained # Command line arguments to be self-contained
# Generate preview of file from selection # Generate preview of file from selection
if [ "${1:-}" = "--preview" ]; then if [ "${1:-}" = "--preview" ]; then
vjfile=$(__filepath_from_selection "$2") vjfile=$(__filepath_from_selection "$2")
__vjournal2json <"$vjfile" | jq -r ".description" | batcat --color=always --style=numbers --language=md __ical2json <"$vjfile" | jq -r ".description" | batcat --color=always --style=numbers --language=md
exit exit
fi fi
# Delete file from selection # Delete file from selection
if [ "${1:-}" = "--delete" ]; then if [ "${1:-}" = "--delete" ]; then
vjfile=$(__filepath_from_selection "$2") vjfile=$(__filepath_from_selection "$2")
rm -v "$vjfile" rm -i "$vjfile"
fi fi
# Generate new entry # Generate new entry
if [ "${1:-}" = "--new" ]; then if [ "${1:-}" = "--new" ]; then
label_new=$(echo "$LABLES" | fzf \ collection=$(echo "$LABLES" | fzf \
--margin 20% \ --margin 20% \
--prompt="Select collection> ") --prompt="Select collection> ")
uuid_new=$(uuidgen) file=""
vjfile_new=$(__filepath_from_selection "$label_new $uuid_new.ics") while [ -f "$file" ] || [ -z "$file" ]; do
if [ -f "$vjfile_new" ]; then uuid=$(uuidgen)
echo "Bad luck..." file=$(__filepath_from_selection "$collection /$uuid.ics")
return 1 done
fi tmpmd=$(mktemp --suffix='.md')
# tmpsha="$tmpmd.sha"
TMPFILE="$(mktemp).md"
SHAFILE="$TMPFILE.sha"
trap 'rm -f "$TMPFILE" "$SHAFILE"' EXIT
{ {
echo "::: Keep this line if you want to add a **JOURNAL** entry (associated to today), else we will add a **NOTE**" echo "::: |> <!-- keep this line to associate the entry to _today_ -->"
echo "# <write summary here>" echo "::: <| <!-- specify the due date for to-dos, can be empty -->"
echo "> <comma-separated list of categories>" echo "# <!-- write summary here -->"
echo "> <!-- comma-separated list of categories -->"
echo "" echo ""
} >"$TMPFILE" } >"$tmpmd"
sha1sum "$TMPFILE" >"$SHAFILE" sha1sum "$tmpmd" >"$tmpsha"
# Open in editor # Open in editor
$EDITOR "$TMPFILE" >/dev/tty $EDITOR "$tmpmd" >/dev/tty
# Update if changes are detected # Update if changes are detected
if ! sha1sum -c "$SHAFILE" >/dev/null 2>&1; then if ! sha1sum -c "$tmpsha" >/dev/null 2>&1; then
vjfile_tmp="$TMPFILE.ics" tmpfile="$tmpmd.ics"
trap 'rm -f "$vjfile_tmp"' EXIT tmpferr="$tmpmd.err"
__vjournalnew "$uuid_new" <"$TMPFILE" >"$vjfile_tmp" && mv "$vjfile_tmp" "$vjfile_new" if __vicalnew "$uuid" <"$tmpmd" >"$tmpfile" 2>"$tmpferr"; then
mv "$tmpfile" "$file"
else
rm "$tmpfile"
less "$tmpferr"
fi fi
rm "$tmpferr"
fi
rm "$tmpmd" "$tmpsha"
fi
# Toggle completed flag
if [ "${1:-}" = "--toggle-completed" ]; then
vtfile=$(__filepath_from_selection "$2")
vtfile_tmp=$(mktemp)
__vtodotogglecompleted "$vtfile" >"$vtfile_tmp" && mv "$vtfile_tmp" "$vtfile" || rm "$vtfile_tmp"
fi
# Increase priority
if [ "${1:-}" = "--increase-priority" ]; then
vtfile=$(__filepath_from_selection "$2")
vtfile_tmp=$(mktemp)
__vtodopriority "$vtfile" "1" >"$vtfile_tmp" && mv "$vtfile_tmp" "$vtfile" || rm "$vtfile_tmp"
fi
# Decrease priority
if [ "${1:-}" = "--decrease-priority" ]; then
vtfile=$(__filepath_from_selection "$2")
vtfile_tmp=$(mktemp)
__vtodopriority "$vtfile" "-1" >"$vtfile_tmp" && mv "$vtfile_tmp" "$vtfile" || rm "$vtfile_tmp"
fi fi
if [ "${1:-}" = "--reload" ]; then if [ "${1:-}" = "--reload" ]; then
__lines __lines
exit exit
fi fi
query="${FZF_QUERY:-}"
if [ "${1:-}" = "--no-completed" ]; then
query="!✅"
fi
if [ "${1:-}" = "--completed" ]; then
query="✅"
fi
if [ "${1:-}" = "--tasks" ]; then
query="✅ | 🔲"
fi
if [ "${1:-}" = "--no-tasks" ]; then
query="!✅ !🔲"
fi
if [ "${1:-}" = "--notes" ]; then
query="🗒️"
fi
if [ "${1:-}" = "--no-notes" ]; then
query="!🗒️"
fi
if [ "${1:-}" = "--journal" ]; then
query="📘"
fi
if [ "${1:-}" = "--no-journal" ]; then
query="!📘"
fi
if [ -z "$query" ]; then
query="!✅"
fi
query=$(echo "$query" | sed 's/ *$//g')
selection=$( selection=$(
__lines | fzf --ansi \ __lines | fzf --ansi \
--query="$query " \
--no-sort \
--no-hscroll \
--ellipsis='' \
--preview="$0 --preview {}" \ --preview="$0 --preview {}" \
--bind="ctrl-d:become($0 --delete {})" \ --bind="ctrl-d:become($0 --delete {})" \
--bind="ctrl-x:become($0 --toggle-completed {})" \
--bind="alt-up:become($0 --increase-priority {})" \
--bind="alt-down:become($0 --decrease-priority {})" \
--bind="ctrl-n:become($0 --new)" \ --bind="ctrl-n:become($0 --new)" \
--bind="alt-0:change-query(!✅)" \
--bind="alt-1:change-query(📘)" \
--bind="alt-2:change-query(🗒️)" \
--bind="alt-3:change-query(✅ | 🔲)" \
--bind="ctrl-s:execute($SYNC_CMD)" \ --bind="ctrl-s:execute($SYNC_CMD)" \
--bind="ctrl-r:reload-sync($0 --reload)" --bind="ctrl-r:reload-sync($0 --reload)"
) )
@@ -342,41 +629,44 @@ if [ -z "$selection" ]; then
return 0 return 0
fi fi
VJ_FILE=$(__filepath_from_selection "$selection") file=$(__filepath_from_selection "$selection")
if [ ! -f "$VJ_FILE" ]; then if [ ! -f "$file" ]; then
echo "ERROR: File '$VJ_FILE' does not exist!" echo "ERROR: File '$file' does not exist!"
return 1 return 1
fi fi
# Parse vjournal file and save as json # Parse vjournal file and save as json
TMPJSON="$(mktemp).json" filejson=$(mktemp)
trap 'rm -f "$TMPJSON"' EXIT __ical2json <"$file" >"$filejson"
__vjournal2json <"$VJ_FILE" >"$TMPJSON"
# Prepare file to be edited # Prepare file to be edited
TMPFILE="$(mktemp).md" filetmp=$(mktemp --suffix='.md')
SHAFILE="$TMPFILE.sha" filesha="$filetmp.sha"
trap 'rm -f "$TMPFILE" "$SHAFILE"' EXIT if jq -e '.due' "$filejson"; then
SUMMARY=$(jq -r '.summary' "$TMPJSON") due=$(jq -r '.due' "$filejson")
CATEGORIES=$(jq -r '.categories | join(",")' "$TMPJSON") echo "::: <| $due" >"$filetmp"
fi
summary=$(jq -r '.summary' "$filejson")
categories=$(jq -r '.categories | join(",")' "$filejson")
{ {
echo "# $SUMMARY" echo "# $summary"
echo "> $CATEGORIES" echo "> $categories"
echo "" echo ""
jq -r '.description' "$TMPJSON" jq -r '.description' "$filejson"
} >"$TMPFILE" } >>"$filetmp"
sha1sum "$TMPFILE" >"$SHAFILE" rm "$filejson"
sha1sum "$filetmp" >"$filesha"
# Open in editor # Open in editor
$EDITOR "$TMPFILE" $EDITOR "$filetmp"
# Update only if changes are detected # Update only if changes are detected
if ! sha1sum -c "$SHAFILE" >/dev/null 2>&1; then if ! sha1sum -c "$filesha" >/dev/null 2>&1; then
echo "Uh... chages detected!" echo "Uh... chages detected!"
vj_file_new="$TMPFILE.ics" vj_file_new="$filetmp.ics"
trap 'rm -f "$vj_file_new"' EXIT __icalupdate "$file" <"$filetmp" >"$vj_file_new" && mv "$vj_file_new" "$file" || rm "$vj_file_new"
__vjournalupdate "$VJ_FILE" <"$TMPFILE" >"$vj_file_new" && mv "$vj_file_new" "$VJ_FILE"
fi fi
rm "$filetmp" "$filesha"
exec "$0" exec "$0"