desktop front ends for typikon

A Quickshell widget for Hyprland, a waybar module and an eww panel, all
reading the same JSON from the typikon CLI so none of them touches the
network and all three agree.

The Quickshell piece is three separable parts: a Typikon singleton that
wraps the CLI and re-reads only when the date rolls over, a Theme singleton
that takes colours from JSON with a default for every key, and the widget
itself.

The legend is built from the week in view rather than hardcoded, because the
levels a tradition uses are its own -- six under the Typikon, three under
Ruthenian particular law -- and the words for them differ too. The church
date shows beside the civil one whenever they differ, which on the old
calendar is always. Forefeasts and afterfeasts get their own line instead of
crowding out the commemoration that makes the day.
This commit is contained in:
funkpower
2026-09-18 09:17:14 -04:00
commit 92b2efbd51
16 changed files with 1317 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Link the front ends into place. Nothing is copied, so a `git pull` updates
# what you are running.
set -euo pipefail
src="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
config="${XDG_CONFIG_HOME:-$HOME/.config}"
binhome="${HOME}/.local/bin"
usage() {
cat <<USAGE
horologion-install [quickshell|waybar|eww|bin|all]
quickshell link quickshell/ into a quickshell config as horologion/
waybar link the bar script and print the module to merge
eww link eww/ into your eww config
bin link horologion-waybar into ~/.local/bin
all bin + whichever of the three are already configured
USAGE
}
link() {
local from="$1" to="$2"
mkdir -p "$(dirname "$to")"
if [ -e "$to" ] && [ ! -L "$to" ]; then
echo " ! $to exists and is not a link — leaving it alone" >&2
return 1
fi
ln -sfn "$from" "$to"
echo " $to -> $from"
}
do_bin() {
link "$src/bin/horologion-waybar" "$binhome/horologion-waybar"
case ":$PATH:" in
*":$binhome:"*) ;;
*) echo " note: $binhome is not on your PATH" >&2 ;;
esac
}
do_quickshell() {
link "$src/quickshell" "$config/quickshell/horologion"
cat <<'NOTE'
Add to your shell.qml:
import qs.horologion.modules
...
Horologion {}
Or run it on its own:
quickshell -p ~/.config/quickshell/horologion/shell.qml
NOTE
}
do_waybar() {
link "$src/waybar/style.css" "$config/waybar/horologion.css"
cat <<'NOTE'
Add "custom/typikon" to a modules list, merge waybar/config.jsonc into
your config, and @import "horologion.css" from your style.css.
NOTE
}
do_eww() {
link "$src/eww" "$config/eww/horologion"
cat <<'NOTE'
Add to your eww.yuck: (include "horologion/horologion.yuck")
Add to your eww.scss: @import "horologion/horologion.scss";
Then put (horologion) in a window, or (horologion-bar) in a bar.
NOTE
}
command -v typikon >/dev/null 2>&1 || {
echo "horologion: typikon is not on PATH." >&2
echo " pip install typikon" >&2
echo " https://git.pumpin.online/funkpower/typikon" >&2
exit 1
}
case "${1:-all}" in
quickshell) do_bin; do_quickshell ;;
waybar) do_bin; do_waybar ;;
eww) do_bin; do_eww ;;
bin) do_bin ;;
all)
do_bin
[ -d "$config/quickshell" ] && do_quickshell
[ -d "$config/waybar" ] && do_waybar
[ -d "$config/eww" ] && do_eww
;;
-h|--help|help) usage ;;
*) usage; exit 2 ;;
esac
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""waybar custom module — the liturgical day in the bar.
"custom/typikon": {
"exec": "horologion-waybar",
"return-type": "json",
"interval": 1800,
"on-click": "typikon --days 7"
}
The class is the fast level, so the bar can colour it: style
`#custom-typikon.xerophagy` and the rest. `.obligatory` is added where a
particular law actually binds.
"""
import argparse
import json
import subprocess
import sys
def run(jurisdiction, days):
cmd = ["typikon", "--json", "--days", str(days)]
if jurisdiction:
cmd += ["--jurisdiction", jurisdiction]
out = subprocess.run(cmd, capture_output=True, text=True, check=True).stdout
parsed = json.loads(out)
return parsed.get("days", [parsed])
def main():
ap = argparse.ArgumentParser()
ap.add_argument("-j", "--jurisdiction", default=None)
ap.add_argument("-n", "--days", type=int, default=7)
ap.add_argument("--no-emoji", action="store_true")
ap.add_argument("--format", default="{emoji} {title}",
help="{emoji} {title} {feast} {fast} {fastLabel} "
"{season} {weekday} {churchDate}")
args = ap.parse_args()
try:
days = run(args.jurisdiction, args.days)
except FileNotFoundError:
print(json.dumps({"text": "typikon?", "tooltip":
"typikon is not on PATH", "class": "error"}))
return 1
except (subprocess.CalledProcessError, ValueError) as exc:
print(json.dumps({"text": "⚠", "tooltip": str(exc), "class": "error"}))
return 1
today = days[0]
fields = dict(today)
if args.no_emoji:
fields["emoji"] = ""
text = args.format.format(**{k: fields.get(k) or "" for k in fields}).strip()
tip = []
head = today["title"]
if today.get("churchDate") and today["churchDate"] not in head:
head += f" ({today['churchDate']})"
tip.append(f"<b>{head}</b>")
if today.get("cycle"):
tip.append(f"<i>{today['cycle']}</i>")
tip.append("")
line = today["fastLabel"]
if today.get("obligation") == "obligatory":
line += " — obligatory"
tip.append(line)
if today.get("reason"):
tip.append(today["reason"])
if today.get("season"):
tip.append(today["season"])
for c in today.get("commemorations", []):
if c != today["title"]:
tip.append(f"· {c}")
if len(days) > 1:
tip.append("")
for d in days[1:]:
tip.append(f"{d['emoji']} {d['weekday'][:3]} {d['title'][:46]}")
tip.append("")
tip.append(f"Pascha {today['pascha']}")
classes = [today["fast"]]
if today.get("obligation") == "obligatory":
classes.append("obligatory")
if today.get("rank"):
classes.append(today["rank"])
print(json.dumps({
"text": text,
"tooltip": "\n".join(tip),
"class": classes,
"alt": today["fast"],
}, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())