Files
menologion/quickshell/modules/Horologion.qml
T
funkpower 92b2efbd51 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.
2026-09-18 09:17:14 -04:00

212 lines
7.2 KiB
QML

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
}
}
}
}