First working version to _edit_ VJOURNAL files

Uses inline python3 code.
This commit is contained in:
2025-05-21 16:55:48 +02:00
parent f01c8dd070
commit 8cd4dedb03

277
fzf-vjour Executable file
View File

@@ -0,0 +1,277 @@
#!/usr/bin/env zsh
set -eu
# Read configuration
CONFIGFILE="$HOME/.config/fzf-vjour/config.yaml"
if [[ ! -e "$CONFIGFILE" ]]
then
echo "Config file '$CONFIGFILE' not found"
exit 1
fi
ROOT=$(yq '.datadir' < "$CONFIGFILE")
if [[ ! -d "$ROOT" ]]
then
echo "Root directory not set or wrongly set"
exit 1
fi
COLLECTION_NAMES=($(yq '.collections [].name' "$CONFIGFILE"))
COLLECTION_LABELS=($(yq '.collections [].label' "$CONFIGFILE"))
COLLECTION_LABEL_MAX_LEN=0
for label in "${COLLECTION_LABELS[@]}"; do
[[ ${#label} -gt $COLLECTION_LABEL_MAX_LEN ]] && COLLECTION_LABEL_MAX_LEN=${#label}
done
__vjourupdate() {
python3 -c '
import sys
from datetime import datetime
from icalendar.cal import Journal
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 = Journal.from_ical(f.read())
except Exception as e:
print(f"Failed to read vjournal file: {e}", file=sys.stderr)
sys.exit(1)
jlist = [component for component in ical.walk("VJOURNAL")]
if len(jlist) == 0:
print("ical file is not a VJOURNAL", file=sys.stderr)
sys.exit(1)
j = jlist[0]
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()
# Update ical
j["SUMMARY"] = summary
j.categories = categories
j["DESCRIPTION"] = description
j.LAST_MODIFIED = datetime.utcnow()
# Print
print(ical.to_ical().decode().replace("\r\n", "\n"))
' "$@"
}
__vjournal2json() {
python3 -c '
import sys
import json
from datetime import datetime
from zoneinfo import ZoneInfo
from icalendar.cal import Journal
input_data = sys.stdin.read()
try:
ical = Journal.from_ical(input_data)
except Exception as e:
print(f"Failed to read vjournal file: {e}", file=sys.stderr)
sys.exit(1)
jlist = [component for component in ical.walk("VJOURNAL")]
if len(jlist) == 0:
sys.exit(0)
j = jlist[0]
local_tz = ZoneInfo("localtime")
data = {
"summary": j.get("SUMMARY"),
"description": j.get("DESCRIPTION"),
"categories": j.categories,
"class": j.get("CLASS"),
"created": str(j.DTSTAMP.astimezone(local_tz)),
"last_modified": str(j.LAST_MODIFIED.astimezone(local_tz)),
"start": str(
j.DTSTART.astimezone(local_tz)
if isinstance(j.DTSTART, datetime)
else j.DTSTART or ""
),
}
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() {
local filepath="$1"
local maxlen="$2"
local totallen=$((maxlen + ${#ROOT}))
local filepathpad=$(printf "%-${totallen}s" "$filepath")
# Color support
local GREEN=$'\033[1;32m'
local WHITE=$'\033[1;97m'
local FAINT=$'\033[2m'
local OFF=$'\033[m';
# Parse file
local 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
done < "$filepath"
# Parse date
if [[ -n $dtstart ]]; then
local date_target=$(date -d "$dtstart" +%s)
local date_today=$(date +%s)
local date_delta=$(( (date_target - date_today) / 86400 ))
local date_expr=$date_delta
if [[ $date_delta -eq 0 ]]
then
date_expr="today"
elif [[ $date_delta -eq -1 ]]
then
date_expr="yesterday"
elif [[ $date_delta -eq 1 ]]
then
date_expr="tomorrow"
elif [[ $date_delta -lt -1 && $date_delta -ge -7 ]]
then
date_expr="last $(date -d $dtstart +%A)"
elif [[ $date_delta -gt 1 && $date_delta -le 7 ]]
then
date_expr="next $(date -d $dtstart +%A)"
else
date_expr=$(date -d $dtstart +%x)
fi
date_emoji="📘"
else
date_emoji="🗒️"
date_expr=""
fi
date_expr=$(printf "%12s" "$date_expr")
# Print line
echo -e "$dtstamp $filepath $WHITE$date_expr$OFF $date_emoji $GREEN$summary$OFF $FAINT$categories$OFF"
}
__lines() {
# Collect all files
local files=()
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find "$ROOT" -type f -name '*.ics' -print0)
# Compute max length of basenames
local maxlen=0
for file in "${files[@]}"; do
local name=$(basename "$file")
[[ ${#name} -gt $maxlen ]] && maxlen=${#name}
done
local lines=$(for file in "${files[@]}"; do
__filepath_to_searchline "$file" "$maxlen"
done)
# Decorate
for ((i = 1; i <= ${#COLLECTION_NAMES[@]}; i++)); do
#local label=$(printf "%${COLLECTION_LABEL_MAX_LEN}s" "${COLLECTION_LABELS[$i]}")
local label="${COLLECTION_LABELS[$i]}"
lines=$(echo "$lines" | sed "s|/*${COLLECTION_NAMES[$i]}/*|:$label |")
done
# Sort and cut off irreleant part
lines=$(echo "$lines" | sort -g -r | cut -d ':' -f 2-)
echo -e "$lines"
}
__filepath_from_selection() {
local selection=$(echo "$1" | cut -d " " -f 1,2)
for ((i = 1; i <= ${#COLLECTION_NAMES[@]}; i++)); do
selection=$(echo "$selection" | sed "s|^.*${COLLECTION_LABELS[$i]} *|$ROOT/${COLLECTION_NAMES[$i]}/|")
done
echo -e "$selection"
}
__open_vj_file() {
local file="$1"
local tmpfile="$(mktemp).md"
echo "$file" | __vjournal2json
}
selection=$(__lines | fzf --ansi)
if [[ -z "$selection" ]]; then
return 0;
fi
VJ_FILE=$(__filepath_from_selection "$selection")
if [[ ! -f "$VJ_FILE" ]]; then
echo "ERROR: File '$VJ_FILE' does not exist!"
return 1;
fi
# Parse vjournal file and save as json
TMPJSON="$(mktemp).json"
trap "rm -f $TMPJSON" EXIT
cat "$VJ_FILE" | __vjournal2json > "$TMPJSON"
# Prepare file to be edited
TMPFILE="$(mktemp).md"
SHAFILE="$TMPFILE.sha"
trap "rm -f $TMPFILE $SHAFILE" EXIT
SUMMARY=$(jq -r '.summary' "$TMPJSON")
CATEGORIES=$(jq -r '.categories | join(",")' "$TMPJSON")
echo "# $SUMMARY" > "$TMPFILE"
echo "> $CATEGORIES" >> "$TMPFILE"
echo >> "$TMPFILE"
jq -r '.description' "$TMPJSON" >> "$TMPFILE"
sha1sum "$TMPFILE" > "$SHAFILE"
# Open in editor
$EDITOR "$TMPFILE"
# Update only if changes are detected
if ! sha1sum -c "$SHAFILE" > /dev/null 2>&1
then
echo "Uh... chages detected!"
local vj_file_new="$TMPFILE.ics"
trap "rm -f $vj_file_new" EXIT
__vjourupdate "$VJ_FILE" < "$TMPFILE" > "$vj_file_new" && mv "$vj_file_new" "$VJ_FILE"
fi
exec "$0"