First working version to _edit_ VJOURNAL files
Uses inline python3 code.
This commit is contained in:
277
fzf-vjour
Executable file
277
fzf-vjour
Executable 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"
|
||||||
Reference in New Issue
Block a user