mirror of
https://git.pumpin.online/funkpower/menologion.git
synced 2026-10-22 18:57:14 +00:00
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:
@@ -0,0 +1,12 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
// Everything tunable in one place.
|
||||
Singleton {
|
||||
readonly property string font: "sans-serif"
|
||||
readonly property int fontSize: 13
|
||||
readonly property int radius: 14
|
||||
readonly property int width: 430
|
||||
readonly property real opacity: 0.82
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"bg": "#1d2021",
|
||||
"surface": "#282828",
|
||||
"overlay": "#504945",
|
||||
"fg": "#ebdbb2",
|
||||
"fgDim": "#a89984",
|
||||
"accent": "#d3869b",
|
||||
"red": "#fb4934",
|
||||
"yellow": "#fabd2f",
|
||||
"green": "#b8bb26"
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs
|
||||
import qs.services
|
||||
|
||||
Panel {
|
||||
id: root
|
||||
title: "Church calendar"
|
||||
note: Typikon.available
|
||||
? (Typikon.season || Typikon.churchDate)
|
||||
: ""
|
||||
|
||||
// The glyph carries what kind of fast it is, so obligation has to be shown
|
||||
// some other way or it disappears with the text: the weekday takes the
|
||||
// colour. Under the Typikon nothing binds and everything is yellow; under
|
||||
// a particular law that does bind, the days that bind go red. A panel that
|
||||
// blurs the two states something untrue.
|
||||
function tint(d) {
|
||||
if (!d) return Theme.fgDim;
|
||||
if (d.fast === "none") return Theme.fgDim;
|
||||
return d.obligation === "obligatory" ? Theme.red : Theme.yellow;
|
||||
}
|
||||
|
||||
// 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 law — and the words for them differ too.
|
||||
readonly property var legend: {
|
||||
const seen = [], out = [];
|
||||
for (const d of Typikon.week) {
|
||||
if (!d.emoji || seen.indexOf(d.fast) !== -1) continue;
|
||||
seen.push(d.fast);
|
||||
out.push({ emoji: d.emoji, label: d.fastLabel });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 7
|
||||
|
||||
Text {
|
||||
visible: !Typikon.available
|
||||
Layout.fillWidth: true
|
||||
text: Typikon.error !== "" ? Typikon.error
|
||||
: "unavailable — is `typikon` on PATH?"
|
||||
color: Theme.fgDim
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 1
|
||||
}
|
||||
|
||||
// Today, given room because it is the day being asked about.
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: Typikon.available
|
||||
spacing: 11
|
||||
|
||||
Text {
|
||||
text: Typikon.emoji
|
||||
font.family: Config.font
|
||||
font.pixelSize: 28
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 1
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: Typikon.title
|
||||
color: Typikon.feast !== "" ? Theme.accent : Theme.fg
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
const bits = [Typikon.weekday];
|
||||
if (Typikon.churchDate !== ""
|
||||
&& Typikon.churchDate !== Qt.formatDate(new Date(), "d MMMM"))
|
||||
bits.push(Typikon.churchDate);
|
||||
if (Typikon.season !== "") bits.push(Typikon.season);
|
||||
if (Typikon.obligatory) bits.push("obligatory");
|
||||
return bits.join(" · ");
|
||||
}
|
||||
color: root.tint(Typikon.today)
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 3
|
||||
font.bold: Typikon.obligatory
|
||||
}
|
||||
|
||||
// Forefeast, afterfeast, leave-taking. These repeat for days at
|
||||
// a time, so they get their own quiet line rather than crowding
|
||||
// out the commemoration that makes the day itself.
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: Typikon.cycle !== ""
|
||||
text: Typikon.cycle
|
||||
color: Qt.alpha(Theme.fgDim, 0.8)
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 3
|
||||
font.italic: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The rest of today's commemorations, under it.
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 39
|
||||
visible: Typikon.available
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: Typikon.commemorations.filter(c => c !== Typikon.title).slice(0, 3)
|
||||
|
||||
delegate: Text {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
text: "· " + modelData
|
||||
color: Theme.fgDim
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
visible: Typikon.week.length > 1
|
||||
implicitHeight: 1
|
||||
color: Qt.alpha(Theme.overlay, 0.6)
|
||||
}
|
||||
|
||||
// The rest of the week: glyph, weekday, and what is kept that day.
|
||||
Repeater {
|
||||
model: Typikon.week.length > 1 ? Typikon.week.slice(1) : []
|
||||
|
||||
delegate: RowLayout {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
spacing: 9
|
||||
|
||||
Text {
|
||||
text: modelData.emoji
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize + 3
|
||||
}
|
||||
Text {
|
||||
Layout.preferredWidth: 32
|
||||
text: modelData.weekday.substring(0, 3)
|
||||
color: root.tint(modelData)
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 2
|
||||
font.bold: modelData.obligation === "obligatory"
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.title
|
||||
color: modelData.feast ? Theme.accent : Theme.fgDim
|
||||
elide: Text.ElideRight
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
visible: Typikon.available
|
||||
implicitHeight: 1
|
||||
color: Qt.alpha(Theme.overlay, 0.6)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: Typikon.available
|
||||
spacing: 10
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
spacing: 9
|
||||
|
||||
Repeater {
|
||||
model: root.legend
|
||||
delegate: Text {
|
||||
required property var modelData
|
||||
text: modelData.emoji + " " + modelData.label
|
||||
color: Qt.alpha(Theme.fgDim, 0.75)
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Typikon.pascha
|
||||
color: Theme.fgDim
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs
|
||||
import qs.services
|
||||
|
||||
// Chrome for the widget. Slightly translucent so a wallpaper reads through it,
|
||||
// opaque enough to stay legible over a busy picture.
|
||||
Rectangle {
|
||||
id: root
|
||||
property string title: ""
|
||||
property string note: ""
|
||||
default property alias content: body.data
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: col.implicitHeight + 28
|
||||
radius: Config.radius
|
||||
color: Qt.alpha(Theme.surface, Config.opacity)
|
||||
border.width: 1
|
||||
border.color: Qt.alpha(Theme.overlay, 0.7)
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 10
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.title !== ""
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.title
|
||||
color: Theme.accent
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: root.note
|
||||
color: Theme.fgDim
|
||||
font.family: Config.font
|
||||
font.pixelSize: Config.fontSize - 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: body
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: children.length > 0 ? children[0].implicitHeight : 0
|
||||
onWidthChanged: if (children.length > 0) children[0].width = width
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
// Colours, read from a JSON file so the widget follows whatever themes the
|
||||
// rest of your desktop.
|
||||
//
|
||||
// Every key has a default, so the widget is legible with no file at all. Point
|
||||
// HOROLOGION_COLORS at your own, or drop one at the path below. The shape is
|
||||
// the flat sixteen-colour kind most theme generators already emit.
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string path: {
|
||||
const env = Quickshell.env("HOROLOGION_COLORS");
|
||||
return env && env !== ""
|
||||
? env
|
||||
: Quickshell.env("HOME") + "/.config/horologion/colors.json";
|
||||
}
|
||||
|
||||
property var c: ({})
|
||||
|
||||
readonly property color bg: c.bg ?? "#1d2021"
|
||||
readonly property color surface: c.surface ?? "#282828"
|
||||
readonly property color overlay: c.overlay ?? "#504945"
|
||||
readonly property color fg: c.fg ?? "#ebdbb2"
|
||||
readonly property color fgDim: c.fgDim ?? "#a89984"
|
||||
readonly property color accent: c.accent ?? "#d3869b"
|
||||
readonly property color red: c.red ?? "#fb4934"
|
||||
readonly property color yellow: c.yellow ?? "#fabd2f"
|
||||
readonly property color green: c.green ?? "#b8bb26"
|
||||
|
||||
function reload() { file.reload(); load(); }
|
||||
|
||||
function load() {
|
||||
try {
|
||||
root.c = JSON.parse(file.text());
|
||||
} catch (e) {
|
||||
root.c = ({});
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: file
|
||||
path: root.path
|
||||
watchChanges: true
|
||||
onFileChanged: root.reload()
|
||||
onLoaded: root.load()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
// The liturgical day, from the `typikon` command.
|
||||
//
|
||||
// Computed locally, so this needs no network and cannot go stale. It only has
|
||||
// to be re-read when the date changes, which a clock at minute precision
|
||||
// notices without polling a subprocess every few seconds.
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Set this to a jurisdiction key — "rocor", "antiochian", "oca" — or leave
|
||||
// it empty to use whatever `typikon --set-default` wrote.
|
||||
property string jurisdiction: ""
|
||||
// Today plus the next six, so the week ahead is visible at a glance.
|
||||
property int days: 7
|
||||
|
||||
property bool available: false
|
||||
property string error: ""
|
||||
|
||||
property var week: []
|
||||
property var today: ({})
|
||||
|
||||
readonly property string date: today.date ?? ""
|
||||
readonly property string weekday: today.weekday ?? ""
|
||||
readonly property string churchDate: today.churchDate ?? ""
|
||||
readonly property string season: today.season ?? ""
|
||||
readonly property string cycle: today.cycle ?? ""
|
||||
readonly property string feast: today.feast ?? ""
|
||||
readonly property string title: today.title ?? ""
|
||||
readonly property string rank: today.rank ?? ""
|
||||
readonly property string fast: today.fast ?? "none"
|
||||
readonly property string fastLabel: today.fastLabel ?? ""
|
||||
readonly property string emoji: today.emoji ?? ""
|
||||
readonly property string reason: today.reason ?? ""
|
||||
readonly property string obligation: today.obligation ?? ""
|
||||
readonly property string pascha: today.pascha ?? ""
|
||||
readonly property int daysToPascha: today.daysToPascha ?? 0
|
||||
readonly property var commemorations: today.commemorations ?? []
|
||||
|
||||
readonly property bool obligatory: obligation === "obligatory"
|
||||
|
||||
function refresh() { if (!proc.running) proc.running = true; }
|
||||
|
||||
SystemClock { id: clock; precision: SystemClock.Minutes }
|
||||
|
||||
// Re-read when the day rolls over, not on a timer. The answer changes at
|
||||
// midnight and at no other moment.
|
||||
readonly property string _day: Qt.formatDateTime(clock.date, "yyyy-MM-dd")
|
||||
on_DayChanged: root.refresh()
|
||||
Component.onCompleted: root.refresh()
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
command: {
|
||||
const c = ["typikon", "--json", "--days", String(root.days)];
|
||||
if (root.jurisdiction !== "")
|
||||
c.push("--jurisdiction", root.jurisdiction);
|
||||
return c;
|
||||
}
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const parsed = JSON.parse(this.text);
|
||||
root.week = parsed.days ?? [parsed];
|
||||
root.today = root.week.length ? root.week[0] : {};
|
||||
root.available = true;
|
||||
root.error = "";
|
||||
} catch (e) {
|
||||
root.available = false;
|
||||
root.error = "could not read typikon";
|
||||
}
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (this.text.trim() !== "") {
|
||||
root.available = false;
|
||||
root.error = this.text.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//@ pragma UseQApplication
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs
|
||||
import qs.modules
|
||||
import qs.services
|
||||
|
||||
// A runnable widget on its own — `quickshell -p quickshell/shell.qml`.
|
||||
//
|
||||
// It sits on the background layer, so it behaves like part of the wallpaper:
|
||||
// always under your windows, never stealing a click. Drop the Horologion
|
||||
// component into an existing shell instead if you already have one.
|
||||
ShellRoot {
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
WlrLayershell.namespace: "horologion"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
anchors { top: true; right: true }
|
||||
margins { top: 24; right: 24 }
|
||||
|
||||
implicitWidth: Config.width
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
|
||||
Horologion {
|
||||
// Leave empty to follow `typikon --set-default`, or name a
|
||||
// jurisdiction here: "rocor", "antiochian", "oca", ...
|
||||
Component.onCompleted: Typikon.jurisdiction = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user