* Answer omarchy-shell calls over the shell's own socket Every omarchy-shell call started a qs ipc client, ~45ms of startup for one IPC call: a theme switch makes two, and every script-driven OSD, toggle refresh and lock query paid it too. The shell now serves a socket in XDG_RUNTIME_DIR, named from its config path and Wayland display as qs ipc selects its instance, and omarchy-shell tries it first through socat, which starts in ~5ms. First-party handlers register as ShellIpc, an IpcHandler that qs ipc still reaches, and the socket calls only the functions a handler declares with their exact argument count, allowed by name so QObject methods such as destroy() stay out of reach. When the shell ran nothing it answers SKIP, and omarchy-shell asks qs ipc for its exact answer, so errors, third-party plugins and an unreachable socket behave as before. A call that may have run is never retried: a timeout or a connection closed without an answer reports the shell as not responding. omarchy-shell shell ping takes ~13-18ms instead of ~61ms, and omarchy-osd reaches the screen in ~36ms instead of ~77ms. Output and exit status match the qs ipc path across 26 calls, errors and quiet mode included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Only accept whole socket replies and retry only unmade connections A reply cut off after its OK prefix passed for the whole answer, and an empty reply with socat failing was retried through qs ipc although the request might already have been delivered. An answer now counts only once its record separator arrived. socat's own errors join the reply, so only its connect error, a socket nothing listens on, falls back to qs ipc beside an explicit SKIP; anything else is reported as not responding rather than retried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
6.3 KiB
QML
186 lines
6.3 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
import qs.Ui
|
|
import "Model.js" as Model
|
|
|
|
// Date/time label for the bar, and the host for the calendar popup.
|
|
//
|
|
// Left click reveals the calendar — asking "what is the date?" is what a
|
|
// click on a clock means — right click walks the common label formats, and
|
|
// middle click opens the timezone picker.
|
|
BarWidget {
|
|
id: root
|
|
moduleName: "omarchy.clock"
|
|
|
|
property date displayDate: clock.date
|
|
|
|
readonly property string configuredFormat: vertical
|
|
? setting("verticalFormat", "HH\n—\nmm")
|
|
: setting("format", "dddd HH:mm")
|
|
readonly property string configuredAltFormat: vertical
|
|
? setting("verticalFormatAlt", "dd\nMMM\n'W'ww\n''yy")
|
|
: setting("formatAlt", "d MMMM 'W'ww yyyy")
|
|
|
|
readonly property var formatRing: Model.clockFormatRing(configuredFormat, configuredAltFormat, Model.clockFormats(vertical))
|
|
|
|
// What the bar shows is what shell.json stores, so a cycled format is the
|
|
// format from then on rather than something that reverts on restart.
|
|
readonly property string activeFormat: configuredFormat
|
|
|
|
// A seconds label needs the clock to tick sixty times as often, and a
|
|
// repaint a second is a price only the formats that print seconds pay.
|
|
readonly property bool showsSeconds: Model.clockNeedsSeconds(activeFormat)
|
|
readonly property string displayText: formatted(displayDate)
|
|
readonly property var verticalLines: displayText.split("\n")
|
|
|
|
function refresh() {
|
|
displayDate = new Date()
|
|
if (panelLoader.item && panelLoader.item.refresh) panelLoader.item.refresh()
|
|
}
|
|
|
|
function cycleFormat() {
|
|
var current = String(configuredFormat)
|
|
var next = Model.nextClockFormat(formatRing, current)
|
|
if (next === "" || next === current) return
|
|
|
|
var entry = { id: root.moduleName }
|
|
for (var key in root.settings) if (key !== "id") entry[key] = root.settings[key]
|
|
entry[vertical ? "verticalFormat" : "format"] = next
|
|
|
|
// Applied locally first so the label changes on the click itself; the
|
|
// shell.json write comes back through the bar as the same value.
|
|
root.settings = entry
|
|
if (root.bar && root.bar.shell && typeof root.bar.shell.updateEntryInline === "function")
|
|
root.bar.shell.updateEntryInline(root.moduleName, entry)
|
|
}
|
|
|
|
function formatted(date) {
|
|
return Qt.formatDateTime(date, activeFormat.replace(/ww/g, Model.isoWeekLiteral(date.getFullYear(), date.getMonth(), date.getDate())))
|
|
}
|
|
|
|
// ---- Calendar popup. Shape contract for shell.summon/hide/toggle
|
|
// routing: Bar.findPanelWidget requires open/close/opened on the
|
|
// bar-widget root.
|
|
readonly property bool opened: panelLoader.item ? panelLoader.item.opened === true : false
|
|
|
|
function open() {
|
|
if (panelLoader.item) panelLoader.item.open()
|
|
}
|
|
|
|
function close() {
|
|
if (panelLoader.item) panelLoader.item.close()
|
|
}
|
|
|
|
function togglePanel() {
|
|
if (panelLoader.item) panelLoader.item.toggle()
|
|
}
|
|
|
|
function toggleWeekStart() {
|
|
if (panelLoader.item) panelLoader.item.toggleWeekStart()
|
|
}
|
|
|
|
// The clock fills more slot than it paints a mark for, at both
|
|
// orientations: horizontally it is a text label in a padded slot, so the
|
|
// dot takes the label width; vertically it is a stack of icon-sized lines,
|
|
// so the dot takes one line — the same mark every icon widget gets, rather
|
|
// than a rule running the height of the whole stack.
|
|
readonly property real openPanelIndicatorWidth: button.labelWidth
|
|
readonly property real openPanelIndicatorHeight: Math.max(Style.space(10), Math.round(Style.bar.iconSlot * 0.55))
|
|
|
|
// Forwarded so this widget can stand in for the panel as the bar's popout
|
|
// identity: Bar.requestPopout prefers closeForPopoutSwitch over close, and
|
|
// KeyboardPanel reads popoutSwitchClosing back off its owner.
|
|
readonly property bool popoutSwitchClosing: panelLoader.item ? panelLoader.item.popoutSwitchClosing === true : false
|
|
|
|
function closeForPopoutSwitch() {
|
|
if (panelLoader.item) panelLoader.item.closeForPopoutSwitch()
|
|
}
|
|
|
|
function injectPanel() {
|
|
var target = panelLoader.item
|
|
if (!target) return
|
|
if ("bar" in target) target.bar = root.bar
|
|
if ("settings" in target) target.settings = root.settings
|
|
if ("anchorItem" in target) target.anchorItem = button
|
|
if ("hostWidget" in target) target.hostWidget = root
|
|
}
|
|
|
|
implicitWidth: button.implicitWidth
|
|
implicitHeight: button.implicitHeight
|
|
|
|
onBarChanged: injectPanel()
|
|
onSettingsChanged: injectPanel()
|
|
|
|
SystemClock {
|
|
id: clock
|
|
precision: root.showsSeconds ? SystemClock.Seconds : SystemClock.Minutes
|
|
onDateChanged: root.displayDate = date
|
|
}
|
|
|
|
Loader {
|
|
id: panelLoader
|
|
active: true
|
|
source: Qt.resolvedUrl("Panel.qml")
|
|
visible: false
|
|
onLoaded: {
|
|
root.injectPanel()
|
|
Qt.callLater(root.injectPanel)
|
|
}
|
|
}
|
|
|
|
ShellIpc {
|
|
target: "omarchy.clock"
|
|
|
|
function refresh(): void { root.broadcast("refresh") }
|
|
function cycleFormat(): void { root.cycleFormat() }
|
|
function toggleWeekStart(): void { root.toggleWeekStart() }
|
|
function open(): void { root.open() }
|
|
function close(): void { root.close() }
|
|
function show(): void { root.open() }
|
|
function hide(): void { root.close() }
|
|
function toggle(): void { root.togglePanel() }
|
|
}
|
|
|
|
WidgetButton {
|
|
id: button
|
|
anchors.fill: parent
|
|
bar: root.bar
|
|
text: root.vertical ? "" : root.displayText
|
|
labelVisible: !root.vertical
|
|
hasVisualContent: root.vertical ? root.verticalLines.length > 0 : text !== ""
|
|
fixedHeight: root.vertical ? root.verticalLines.length * Style.bar.iconSlot : -1
|
|
horizontalMargin: 8.75
|
|
verticalPadding: 8.75
|
|
tooltipText: "Right-click to toggle format"
|
|
|
|
onPressed: function(b) {
|
|
if (b === Qt.RightButton) root.cycleFormat()
|
|
else if (b === Qt.MiddleButton) { if (root.bar) root.bar.run("omarchy-menu-timezone") }
|
|
else root.togglePanel()
|
|
}
|
|
|
|
Column {
|
|
visible: root.vertical
|
|
anchors.fill: parent
|
|
|
|
Repeater {
|
|
model: root.verticalLines
|
|
|
|
OpticalGlyph {
|
|
required property string modelData
|
|
width: button.width
|
|
height: Style.bar.iconSlot
|
|
text: modelData
|
|
fontFamily: button.fontFamily
|
|
fontSize: modelData.length > 3
|
|
? button.fontSize * 0.9
|
|
: button.fontSize
|
|
color: button.foreground
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|