* 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>
500 lines
15 KiB
QML
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)
|
|
}
|
|
}
|
|
}
|