Files
omarchy/shell/plugins/bar/widgets/Indicators.qml
T
David Heinemeier HanssonandClaude Fable 5.1 9d48fc402e Install gliff by default and show when a remote session is active (#14712)
* Install gliff by default

Gliff is the Hyprland remote desktop over SSH. Ship it in the base
package set for new installs, with a migration for existing ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Show a bar indicator while a gliff remote session is active

The machine being controlled should make it obvious that someone is on
it. A first-party remote-session service watches Hyprland's screencast
events, which gliff-server triggers through ext-image-copy-capture, and
probes for the server process, picking up the ssh client address from
its environment. The RemoteSession indicator in the default tray lights
up in the urgent colour while a session runs, and a notification
announces each session start and end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Address review of the remote session indicator

Probe only the current user's gliff-server processes, forward refresh
through the third-party service proxy so cloned indicator widgets can
re-probe, exercise the Hyprland screencast event path in the service
test with a grim capture instead of calling refresh by hand, and
describe the indicator as status only in the manual.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Drop the remote session notifications

The bar indicator is the signal; a toast on every connect and
disconnect is noise on top of it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Tighten remote session detection after review

Hyprland reports a screencast stopped as soon as frames pause for half a
second and says nothing more when the server later exits, so poll every
two seconds while a session is active instead of trusting a stop event.
Match only the screencast event, not its screencastv2 twin, and debounce
the burst around one transition so it costs one probe. Move the probe
into its own script that the service and the test both run, poll for
state in the service test instead of fixed delays, and drop the unused
session count.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 17:02:42 +02:00

500 lines
15 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: [ "PasswordlessSudo", "ScreenRecording", "RemoteSession", "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()
// Only the tree for the current orientation exists (see orientationLoader
// below); its wrapper keeps the stock sizing expressions, so the root's
// implicit size still follows the blocks synchronously rather than waiting
// on a positioner's polish.
implicitWidth: orientationLoader.item ? orientationLoader.item.implicitWidth : 0
implicitHeight: orientationLoader.item ? orientationLoader.item.implicitHeight : 0
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()
// Instantiating both orientation trees and toggling `visible` doubled every
// indicator per bar (and every process an indicator spawns, once per
// monitor). Load only the tree that matches the bar orientation instead.
Loader {
id: orientationLoader
sourceComponent: root.vertical ? verticalIndicatorsTree : horizontalIndicatorsTree
}
Component {
id: horizontalIndicatorsTree
Item {
implicitWidth: activeHorizontalBlock.implicitWidth + inactiveHorizontalArea.implicitWidth
implicitHeight: Math.max(activeHorizontalBlock.implicitHeight, inactiveHorizontalArea.implicitHeight)
width: implicitWidth
height: implicitHeight
Row {
id: horizontalIndicators
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
}
}
}
}
Component {
id: verticalIndicatorsTree
Item {
implicitWidth: Math.max(activeVerticalBlock.implicitWidth, inactiveVerticalArea.implicitWidth)
implicitHeight: activeVerticalBlock.implicitHeight + inactiveVerticalArea.implicitHeight
width: implicitWidth
height: implicitHeight
Column {
id: verticalIndicators
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)
}
}
}