Files
omarchy/shell/plugins/osd/Osd.qml
T
David Heinemeier HanssonandClaude Opus 5.5 c231097df7 Answer omarchy-shell calls over the shell's own socket (#13435)
* 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>
2026-09-27 20:46:00 +02:00

203 lines
7.3 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Ui
import "OsdModel.js" as OsdModel
Item {
id: root
property bool opened: false
property string icon: ""
property string message: ""
property string iconKey: ""
property int value: 0
property int maxValue: 100
property bool hasProgress: true
property int duration: 1200
readonly property bool mediaOsd: iconKey.indexOf("media") === 0 || iconKey.indexOf("player") === 0
// The card is built out of measured columns instead of fixed widths, so it
// keeps exactly `pad` between border and content on every side whatever
// glyph or message it carries. Messages grow with their text up to
// `maxMessageWidth` and elide beyond it.
readonly property int pad: Style.space(16)
readonly property int gap: Style.space(16)
// A glyph next to a message reads airier than it measures: the icon outline
// and the letterforms both fall away from their ink extremes, so the space
// between them opens up well past the nominal gap. Text takes two thirds of
// it; the progress bar's hard edge keeps the full gap.
readonly property int messageGap: Math.round(root.gap * 2 / 3)
readonly property int barWidth: Style.space(142)
readonly property int maxMessageWidth: root.mediaOsd ? Style.space(325) : Style.space(190)
// Nerd Font glyphs draw well outside their monospace cell, so the icon
// column is measured by ink rather than by advance width. Progress OSDs pin
// it to the widest glyph the model can return, so the bar doesn't shift when
// volume crosses an icon threshold.
readonly property int iconInkWidth: Math.ceil(iconMetrics.tightBoundingRect.width)
readonly property int iconWidth: root.hasProgress
? Math.max(root.iconInkWidth, Math.ceil(widestIconMetrics.tightBoundingRect.width))
: root.iconInkWidth
// Same idea for the readout: it is as wide as the longest percentage so the
// digits don't jitter between 9% and 100%.
readonly property int valueWidth: Math.ceil(Math.max(valueMetrics.advanceWidth, messageMetrics.advanceWidth))
readonly property int messageWidth: Math.min(Math.ceil(messageMetrics.advanceWidth), root.maxMessageWidth)
readonly property int contentWidth: root.hasProgress
? root.iconWidth + root.gap + root.barWidth + root.gap + root.valueWidth
: (root.message === "" ? root.iconWidth : root.iconWidth + root.messageGap + root.messageWidth)
function iconFor(name, percent) {
return OsdModel.iconFor(name, percent)
}
function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) {
var next = OsdModel.stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration)
// Update before opening so a fresh OSD starts at its new value; only
// subsequent updates while it remains open animate the progress bar.
iconKey = next.iconKey
maxValue = next.maxValue
hasProgress = next.hasProgress
value = next.value
message = next.message
icon = next.icon
duration = next.duration
opened = true
if (duration > 0) hideTimer.restart()
else hideTimer.stop()
}
function open(payloadJson) {
try {
var p = JSON.parse(payloadJson || "{}")
show(p.icon || "", p.message || "", p.value === undefined ? "" : String(p.value), p.max === undefined ? "100" : String(p.max), p.progressText || "", p.duration === undefined ? "1200" : String(p.duration))
} catch (e) {}
}
function close() { opened = false }
Timer {
id: hideTimer
interval: root.duration
onTriggered: root.opened = false
}
TextMetrics {
id: messageMetrics
font.family: Style.font.family
font.bold: true
font.pixelSize: Style.font.title
text: root.message
}
TextMetrics {
id: valueMetrics
font: messageMetrics.font
text: "100%"
}
TextMetrics {
id: iconMetrics
font.family: Style.font.family
font.pixelSize: Style.font.displayLarge
text: root.icon
}
TextMetrics {
id: widestIconMetrics
font: iconMetrics.font
text: OsdModel.widestIcon
}
ShellIpc {
target: "osd"
function show(payloadJson: string): string {
root.open(payloadJson)
return "ok"
}
function close(): string { root.close(); return "ok" }
function state(): string { return root.opened ? "open" : "closed" }
function ping(): string { return "ok" }
}
OverlayWindow {
id: panel
shown: root.opened
shownKeyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "omarchy-osd"
// Visual-only surface: keep the layer-shell input region empty so the OSD
// never blocks clicks to the desktop below it.
mask: Region {}
BorderSurface {
id: card
width: card.borderLeft + root.pad + root.contentWidth + root.pad + card.borderRight
height: card.borderTop + root.pad + Style.font.displayLarge + root.pad + card.borderBottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.space(67)
color: Util.alpha(Color.background, 0.97)
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2)))
radius: Style.cornerRadius
opacity: root.opened ? 1 : 0
Row {
anchors.fill: parent
anchors.topMargin: card.borderTop + root.pad
anchors.rightMargin: card.borderRight + root.pad
anchors.bottomMargin: card.borderBottom + root.pad
anchors.leftMargin: card.borderLeft + root.pad
spacing: root.hasProgress ? root.gap : root.messageGap
Item {
width: root.iconWidth
height: parent.height
Text {
textFormat: Text.PlainText
// Sit the glyph's ink flush in the column, centered when the
// column is wider than this particular glyph.
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
anchors.verticalCenter: parent.verticalCenter
text: root.icon
font: iconMetrics.font
color: Color.popups.text
}
}
Rectangle {
visible: root.hasProgress
width: root.barWidth
height: Math.max(Style.space(6), Style.spacing.sm)
anchors.verticalCenter: parent.verticalCenter
color: Util.alpha(Color.popups.text, 0.45)
Rectangle {
height: parent.height
width: parent.width * (root.hasProgress ? root.value / root.maxValue : 0)
color: Color.accent
Behavior on width {
enabled: root.opened
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
}
}
Text {
textFormat: Text.PlainText
visible: root.message !== ""
width: root.hasProgress ? root.valueWidth : root.messageWidth
// The readout hugs the card edge so a short percentage doesn't leave
// a hole in the padding; the slack lands in the gap after the bar.
horizontalAlignment: root.hasProgress ? Text.AlignRight : Text.AlignLeft
anchors.verticalCenter: parent.verticalCenter
text: root.message
font: messageMetrics.font
color: Color.popups.text
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}