Files
omarchy/shell/plugins/bar/widgets/Indicators.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

472 lines
13 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Ui
BarWidget {
id: root
moduleName: "omarchy.indicators"
readonly property var defaultIndicatorEntries: [ "Dictation", "ScreenRecording", "Reminder", "NightLight", "Dnd", "StayAwake" ]
readonly property var indicatorEntries: indicatorEntriesFromSettings(settings)
property var activeIndicatorIds: []
property var indicatorActiveStates: ({})
property bool indicatorAreaHovered: false
property bool indicatorItemHovered: false
readonly property bool alwaysShowIndicators: setting("alwaysShow", false) === true
readonly property bool revealInactiveIndicators: alwaysShowIndicators || indicatorAreaHovered || indicatorItemHovered || (bar && bar.centerSectionRevealHeld === true && bar.centerHoverRevealSuppressed !== true)
signal refreshRequested()
ListModel { id: activeIndicatorModel }
function entryId(entry) {
if (typeof entry === "string") return entry
if (Util.isPlainObject(entry)) {
var id = entry["id"]
if (id !== undefined && id !== null && String(id) !== "") return String(id)
}
return ""
}
function entrySettings(entry) {
if (!Util.isPlainObject(entry)) return {}
var copy = {}
for (var key in entry) {
if (key === "id") continue
copy[key] = entry[key]
}
return copy
}
function indicatorEntriesFromSettings(settings) {
var source = defaultIndicatorEntries
if (settings.items && typeof settings.items.length === "number" && settings.items.length > 0) source = settings.items
else if (settings.indicators && typeof settings.indicators.length === "number" && settings.indicators.length > 0) source = settings.indicators
var result = []
for (var i = 0; i < source.length; i++) {
var item = source[i]
if (typeof item !== "string" && item !== null && typeof item === "object") {
try {
item = JSON.parse(JSON.stringify(item))
} catch (error) {
}
}
var id = entryId(item)
if (id !== "") result.push(item)
}
return result
}
function setIndicatorAreaHovered(hovered) {
indicatorAreaHovered = hovered
if (hovered) indicatorHideTimer.stop()
else indicatorHideTimer.restart()
}
function setIndicatorItemHovered(hovered) {
if (hovered) {
indicatorItemHovered = true
indicatorHideTimer.stop()
} else {
indicatorHideTimer.restart()
}
}
function hasIndicatorId(id) {
for (var i = 0; i < indicatorEntries.length; i++) {
if (entryId(indicatorEntries[i]) === id) return true
}
return false
}
function entryForId(id) {
for (var i = 0; i < indicatorEntries.length; i++) {
var entry = indicatorEntries[i]
if (entryId(entry) === id) return entry
}
return { id: id }
}
function activeModelIndex(id) {
for (var i = 0; i < activeIndicatorModel.count; i++) {
if (activeIndicatorModel.get(i).activeId === id) return i
}
return -1
}
function copyActiveStates() {
var states = {}
for (var id in indicatorActiveStates) {
if (indicatorActiveStates[id] === true) states[id] = true
}
return states
}
function orderedActiveIds(states, preferredOrder) {
var ids = []
for (var i = 0; i < preferredOrder.length; i++) {
var id = preferredOrder[i]
if (ids.indexOf(id) === -1 && hasIndicatorId(id) && states[id] === true) ids.push(id)
}
return ids
}
function syncActiveIndicatorModel() {
for (var i = activeIndicatorModel.count - 1; i >= 0; i--) {
if (activeIndicatorIds.indexOf(activeIndicatorModel.get(i).activeId) === -1)
activeIndicatorModel.remove(i)
}
for (var j = 0; j < activeIndicatorIds.length; j++) {
var id = activeIndicatorIds[j]
var index = activeModelIndex(id)
if (index === -1) activeIndicatorModel.insert(j, { activeId: id })
else if (index !== j) activeIndicatorModel.move(index, j, 1)
}
}
function setIndicatorActive(entry, active) {
var id = entryId(entry)
if (id === "") return
var states = copyActiveStates()
if (active) states[id] = true
else delete states[id]
indicatorActiveStates = states
var ids = orderedActiveIds(states, activeIndicatorIds)
// The active block sits closest to the clock, so newcomers go on the far
// side of it. Appending would shove everything already showing sideways.
if (active && ids.indexOf(id) === -1 && hasIndicatorId(id)) ids.unshift(id)
activeIndicatorIds = ids
syncActiveIndicatorModel()
}
function syncActiveIndicatorOrder() {
activeIndicatorIds = orderedActiveIds(indicatorActiveStates, activeIndicatorIds)
syncActiveIndicatorModel()
}
function refresh() { root.refreshRequested() }
onIndicatorEntriesChanged: syncActiveIndicatorOrder()
implicitWidth: root.vertical
? Math.max(activeVerticalBlock.implicitWidth, inactiveVerticalArea.implicitWidth)
: activeHorizontalBlock.implicitWidth + inactiveHorizontalArea.implicitWidth
implicitHeight: root.vertical
? activeVerticalBlock.implicitHeight + inactiveVerticalArea.implicitHeight
: Math.max(activeHorizontalBlock.implicitHeight, inactiveHorizontalArea.implicitHeight)
ShellIpc {
target: "omarchy.indicators"
function refresh(): void {
root.broadcast("refresh")
}
}
Timer {
id: indicatorHideTimer
interval: 120
onTriggered: {
if (!root.indicatorAreaHovered)
root.indicatorItemHovered = false
}
}
Component.onCompleted: root.refreshRequested()
Row {
id: horizontalIndicators
visible: !root.vertical
spacing: 0
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
Item {
id: inactiveHorizontalArea
implicitWidth: root.revealInactiveIndicators ? inactiveHorizontalBlock.implicitWidth : 0
implicitHeight: Math.max(inactiveHorizontalBlock.implicitHeight, root.barSize)
width: implicitWidth
height: implicitHeight
clip: true
IndicatorBlock {
id: inactiveHorizontalBlock
anchors.verticalCenter: parent.verticalCenter
indicatorsModule: root
indicatorEntries: root.indicatorEntries
indicatorBlock: "inactive"
horizontal: true
reportActiveState: !root.vertical
}
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
}
ActiveIndicatorBlock {
id: activeHorizontalBlock
indicatorsModule: root
indicatorModel: activeIndicatorModel
horizontal: true
reportActiveState: !root.vertical
}
}
Column {
id: verticalIndicators
visible: root.vertical
spacing: 0
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
Item {
id: inactiveVerticalArea
implicitWidth: Math.max(inactiveVerticalBlock.implicitWidth, root.barSize)
implicitHeight: root.revealInactiveIndicators ? inactiveVerticalBlock.implicitHeight : 0
width: implicitWidth
height: implicitHeight
clip: true
IndicatorBlock {
id: inactiveVerticalBlock
anchors.horizontalCenter: parent.horizontalCenter
indicatorsModule: root
indicatorEntries: root.indicatorEntries
indicatorBlock: "inactive"
horizontal: false
reportActiveState: root.vertical
}
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
}
ActiveIndicatorBlock {
id: activeVerticalBlock
indicatorsModule: root
indicatorModel: activeIndicatorModel
horizontal: false
reportActiveState: root.vertical
}
}
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
component ActiveIndicatorBlock: Item {
id: activeIndicatorBlockRoot
property var indicatorModel: null
property var indicatorsModule: null
property bool horizontal: true
property bool reportActiveState: false
implicitWidth: blockLoader.item ? blockLoader.item.implicitWidth : 0
implicitHeight: blockLoader.item ? blockLoader.item.implicitHeight : 0
width: implicitWidth
height: implicitHeight
Loader {
id: blockLoader
anchors.centerIn: parent
sourceComponent: activeIndicatorBlockRoot.horizontal ? horizontalActiveIndicatorBlock : verticalActiveIndicatorBlock
}
Component {
id: horizontalActiveIndicatorBlock
Row {
spacing: 0
Repeater {
model: activeIndicatorBlockRoot.indicatorModel
IndicatorLoader {
required property string activeId
indicatorsModule: activeIndicatorBlockRoot.indicatorsModule
entry: activeIndicatorBlockRoot.indicatorsModule.entryForId(activeId)
indicatorBlock: "active"
reportActiveState: activeIndicatorBlockRoot.reportActiveState
}
}
}
}
Component {
id: verticalActiveIndicatorBlock
Column {
spacing: 0
Repeater {
model: activeIndicatorBlockRoot.indicatorModel
IndicatorLoader {
required property string activeId
indicatorsModule: activeIndicatorBlockRoot.indicatorsModule
entry: activeIndicatorBlockRoot.indicatorsModule.entryForId(activeId)
indicatorBlock: "active"
reportActiveState: activeIndicatorBlockRoot.reportActiveState
}
}
}
}
}
component IndicatorBlock: Item {
id: indicatorBlockRoot
property var indicatorEntries: []
property var indicatorsModule: null
property string indicatorBlock: "active"
property bool horizontal: true
property bool reportActiveState: false
implicitWidth: blockLoader.item ? blockLoader.item.implicitWidth : 0
implicitHeight: blockLoader.item ? blockLoader.item.implicitHeight : 0
width: implicitWidth
height: implicitHeight
Loader {
id: blockLoader
anchors.centerIn: parent
sourceComponent: indicatorBlockRoot.horizontal ? horizontalIndicatorBlock : verticalIndicatorBlock
}
Component {
id: horizontalIndicatorBlock
Row {
spacing: 0
Repeater {
model: indicatorBlockRoot.indicatorEntries
IndicatorLoader {
required property var modelData
indicatorsModule: indicatorBlockRoot.indicatorsModule
entry: modelData
indicatorBlock: indicatorBlockRoot.indicatorBlock
reportActiveState: indicatorBlockRoot.reportActiveState
}
}
}
}
Component {
id: verticalIndicatorBlock
Column {
spacing: 0
Repeater {
model: indicatorBlockRoot.indicatorEntries
IndicatorLoader {
required property var modelData
indicatorsModule: indicatorBlockRoot.indicatorsModule
entry: modelData
indicatorBlock: indicatorBlockRoot.indicatorBlock
reportActiveState: indicatorBlockRoot.reportActiveState
}
}
}
}
}
component IndicatorLoader: Item {
id: indicatorSlot
required property var entry
property var indicatorsModule: null
required property string indicatorBlock
property bool reportActiveState: false
property bool activeStateObserved: false
readonly property string indicatorId: root.entryId(entry)
readonly property var indicatorSettings: root.entrySettings(entry)
readonly property var barRef: root.bar
implicitWidth: indicatorSource.item && indicatorSource.item.visible ? indicatorSource.item.implicitWidth : 0
implicitHeight: indicatorSource.item && indicatorSource.item.visible ? indicatorSource.item.implicitHeight : 0
width: implicitWidth
height: implicitHeight
onEntryChanged: {
activeStateObserved = false
injectProps()
syncActiveState()
}
onIndicatorBlockChanged: injectProps()
onIndicatorSettingsChanged: injectProps()
onIndicatorsModuleChanged: {
injectProps()
syncActiveState()
}
onReportActiveStateChanged: syncActiveState()
onBarRefChanged: injectProps()
Loader {
id: indicatorSource
anchors.fill: parent
source: indicatorSlot.indicatorId ? Qt.resolvedUrl("../indicators/" + indicatorSlot.indicatorId + ".qml") : ""
onLoaded: {
indicatorSlot.injectProps()
indicatorSlot.syncActiveState()
}
onStatusChanged: if (status === Loader.Error) console.warn("Indicator loader error", indicatorSlot.indicatorId, source)
}
Connections {
target: indicatorSource.item
ignoreUnknownSignals: true
function onActiveChanged() { indicatorSlot.syncActiveState() }
}
function injectProps() {
var target = indicatorSource.item
if (!target) return
if ("bar" in target) target.bar = root.bar
if ("moduleName" in target) target.moduleName = indicatorId
if ("settings" in target) target.settings = indicatorSettings
if ("indicatorBlock" in target) target.indicatorBlock = indicatorBlock
if ("indicatorHost" in target) target.indicatorHost = root
if ("activeOverride" in target) target.activeOverride = indicatorBlock === "active" ? true : null
}
function syncActiveState() {
if (!reportActiveState || !indicatorsModule || !indicatorsModule.setIndicatorActive) return
var active = !!indicatorSource.item && indicatorSource.item.active === true
if (indicatorBlock === "active") {
if (active) activeStateObserved = true
else if (!activeStateObserved) return
}
indicatorsModule.setIndicatorActive(entry, active)
}
}
}