* 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>
472 lines
13 KiB
QML
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)
|
|
}
|
|
}
|
|
}
|