commit 460e2ebaa881d0b955e03a5505b934e801427af1 Author: Andre Padez Date: Mon Aug 17 01:48:32 2026 +0100 Baseline: omarchy.indicators as shipped Indicators.qml and the six indicator files exactly as they ship in omarchy-dev 4.0.0.r1744.gf002044, alongside the manifest `omarchy plugin clone` generates. Kept on its own branch so `git diff stock..master` shows precisely what this fork changes. diff --git a/Indicators.qml b/Indicators.qml new file mode 100644 index 0000000..a633000 --- /dev/null +++ b/Indicators.qml @@ -0,0 +1,471 @@ +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) + + IpcHandler { + 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) + } + } +} diff --git a/indicators/Dictation.qml b/indicators/Dictation.qml new file mode 100644 index 0000000..57f3c31 --- /dev/null +++ b/indicators/Dictation.qml @@ -0,0 +1,38 @@ +import QtQuick +import Quickshell.Io +import qs.Ui + +BarIndicator { + id: root + + property string state: "idle" + property string icon: "" + + active: state === "recording" + activeText: icon + inactiveText: "󰍬" + activeTooltipText: state + inactiveTooltipText: "Dictate" + + function update(raw) { + var data = extractData(raw) + + state = String(data.alt || data.class || "idle") + if (state === "recording") icon = "󰍬" + else if (state === "transcribing") icon = "󰔟" + else icon = "" + } + + Process { + command: ["bash", "-c", "omarchy-voxtype-status"] + running: true + stdout: SplitParser { + onRead: function(data) { root.update(data) } + } + } + + onPressed: function() { + if (!root.bar) return + root.bar.run("omarchy-voxtype-config") + } +} diff --git a/indicators/Dnd.qml b/indicators/Dnd.qml new file mode 100644 index 0000000..052469b --- /dev/null +++ b/indicators/Dnd.qml @@ -0,0 +1,22 @@ +import QtQuick +import qs.Commons +import qs.Ui + +BarIndicator { + id: root + + readonly property var notificationService: bar?.shell?.firstPartyServiceFor("omarchy.notifications") + readonly property bool dnd: notificationService ? notificationService.doNotDisturb : false + + active: dnd + activeText: "󰂛" + inactiveText: "󰂛" + activeTooltipText: "Allow Notifications" + inactiveTooltipText: "Silence Notifications" + + onPressed: function() { + if (root.notificationService) { + root.notificationService.setDoNotDisturb(!root.notificationService.doNotDisturb) + } + } +} diff --git a/indicators/NightLight.qml b/indicators/NightLight.qml new file mode 100644 index 0000000..b15744c --- /dev/null +++ b/indicators/NightLight.qml @@ -0,0 +1,20 @@ +import QtQuick +import qs.Ui + +BarIndicator { + id: root + + readonly property var nightlightService: bar?.shell?.firstPartyServiceFor("omarchy.nightlight") + + active: nightlightService ? nightlightService.enabled : false + activeText: "󰔎" + inactiveText: "󰔎" + activeTooltipText: "Day Light" + inactiveTooltipText: "Night Light" + + function toggle() { + if (root.nightlightService) root.nightlightService.setNightlight(!root.active) + } + + onPressed: function() { root.toggle() } +} diff --git a/indicators/Reminder.qml b/indicators/Reminder.qml new file mode 100644 index 0000000..3441edb --- /dev/null +++ b/indicators/Reminder.qml @@ -0,0 +1,59 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Ui + +BarIndicator { + id: root + + property int reminderCount: 0 + property string tooltip: "" + + active: reminderCount > 0 + activeText: "󰢌" + inactiveText: "󰢌" + activeTooltipText: tooltip + inactiveTooltipText: tooltip + + function refresh() { + if (!jsonProc.running) jsonProc.running = true + } + + function openReminderFlow() { + Quickshell.execDetached(["omarchy-reminder", "-i"]) + } + + function update(raw) { + var data = extractData(raw) + reminderCount = Number(data.count || 0) + tooltip = String(data.tooltip || "") + } + + Component.onCompleted: refresh() + + Connections { + target: root.indicatorHost + ignoreUnknownSignals: true + function onRefreshRequested() { root.refresh() } + } + + Process { + id: jsonProc + command: ["omarchy-reminder", "show", "--json"] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.update(text) + } + onExited: function(exitCode) { + if (exitCode !== 0) { + root.reminderCount = 0 + root.tooltip = "" + } + } + } + + onPressed: function() { + if (root.reminderCount > 0) Quickshell.execDetached(["omarchy-reminder", "show"]) + else root.openReminderFlow() + } +} diff --git a/indicators/ScreenRecording.qml b/indicators/ScreenRecording.qml new file mode 100644 index 0000000..271e1c3 --- /dev/null +++ b/indicators/ScreenRecording.qml @@ -0,0 +1,43 @@ +import QtQuick +import Quickshell.Io +import qs.Ui + +BarIndicator { + id: root + + property bool recording: false + + active: recording + activeText: "󰻂" + inactiveText: "󰻂" + activeTooltipText: "Stop recording" + inactiveTooltipText: "Screen Recording" + + function refresh() { + if (!root.bar || statusProc.running) return + statusProc.command = ["pgrep", "--quiet", "-f", "^gpu-screen-recorder"] + statusProc.running = true + } + + onBarChanged: refresh() + Component.onCompleted: refresh() + + Connections { + target: root.indicatorHost + ignoreUnknownSignals: true + function onRefreshRequested() { root.refresh() } + } + + Process { + id: statusProc + onExited: function(exitCode) { + root.recording = exitCode === 0 + } + } + + onPressed: function() { + if (root.bar) { + root.bar.run(root.recording ? "omarchy-capture-screenrecording --stop-recording" : "omarchy-menu toggle trigger.capture.screenrecord") + } + } +} diff --git a/indicators/StayAwake.qml b/indicators/StayAwake.qml new file mode 100644 index 0000000..70a7829 --- /dev/null +++ b/indicators/StayAwake.qml @@ -0,0 +1,20 @@ +import QtQuick +import qs.Ui + +BarIndicator { + id: root + + readonly property var idleService: bar?.shell?.firstPartyServiceFor("omarchy.idle") + + active: idleService ? idleService.stayAwake : false + activeText: "󰅶" + inactiveText: "󰅶" + activeTooltipText: "Allow Idle Lock & Screensaver" + inactiveTooltipText: "Stay Awake" + + function toggle() { + if (root.idleService) root.idleService.setIdleEnabled(root.active) + } + + onPressed: function() { root.toggle() } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..29446ce --- /dev/null +++ b/manifest.json @@ -0,0 +1,73 @@ +{ + "schemaVersion": 1, + "id": "pastilhas.indicators", + "name": "Indicators", + "version": "1.0.0", + "author": "pastilhas", + "description": "Manual state indicators", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "Indicators.qml" + }, + "barWidget": { + "displayName": "Indicators", + "description": "Manual state indicators", + "category": "Status", + "allowMultiple": true, + "schema": [ + { + "key": "items", + "type": "multiselect", + "label": "Indicators", + "description": "Choose which indicators this widget instance should show. Leave empty to show all indicators.", + "noSelectionText": "All indicators", + "placeholderText": "Search indicators...", + "emptyText": "No indicators", + "options": [ + { + "value": "Dictation", + "label": "Dictation", + "description": "Voice typing status" + }, + { + "value": "ScreenRecording", + "label": "Screen recording", + "description": "GPU screen recorder status" + }, + { + "value": "Reminder", + "label": "Reminder", + "description": "Queued reminder status" + }, + { + "value": "NightLight", + "label": "Night light", + "description": "Blue-light filter" + }, + { + "value": "Dnd", + "label": "Do not disturb", + "description": "Notification silencing" + }, + { + "value": "StayAwake", + "label": "Stay awake", + "description": "Idle lock and screensaver override" + } + ] + }, + { + "key": "alwaysShow", + "type": "boolean", + "label": "Always Show", + "description": "Show inactive indicators without waiting for hover.", + "defaultValue": false + } + ] + }, + "omarchy": { + "clonedFrom": "omarchy.indicators" + } +}