* 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>
238 lines
7.2 KiB
QML
238 lines
7.2 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
|
|
ShellRoot {
|
|
id: root
|
|
|
|
property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT")
|
|
property var failures: []
|
|
property var commands: []
|
|
|
|
function fail(message) {
|
|
failures.push(String(message))
|
|
}
|
|
|
|
function assertTrue(condition, message) {
|
|
if (!condition) fail(message)
|
|
}
|
|
|
|
QtObject {
|
|
id: notificationService
|
|
property bool doNotDisturb: false
|
|
function setDoNotDisturb(value) {
|
|
doNotDisturb = !!value
|
|
}
|
|
}
|
|
|
|
QtObject {
|
|
id: idleService
|
|
property bool stayAwake: false
|
|
readonly property bool idleEnabled: !stayAwake
|
|
function setIdleEnabled(value) {
|
|
stayAwake = !value
|
|
}
|
|
}
|
|
|
|
QtObject {
|
|
id: nightlightService
|
|
property bool enabled: false
|
|
function setNightlight(value) {
|
|
enabled = !!value
|
|
}
|
|
}
|
|
|
|
QtObject {
|
|
id: remoteSessionService
|
|
property bool active: false
|
|
property var peers: []
|
|
property int refreshes: 0
|
|
function refresh() {
|
|
refreshes++
|
|
}
|
|
}
|
|
|
|
QtObject {
|
|
id: mockShell
|
|
function firstPartyServiceFor(id) {
|
|
if (id === "omarchy.notifications") return notificationService
|
|
if (id === "omarchy.idle") return idleService
|
|
if (id === "omarchy.nightlight") return nightlightService
|
|
if (id === "omarchy.remote-session") return remoteSessionService
|
|
return null
|
|
}
|
|
}
|
|
|
|
QtObject {
|
|
id: mockBar
|
|
property bool vertical: false
|
|
property int barSize: 26
|
|
property string fontFamily: "monospace"
|
|
property color barForeground: "white"
|
|
property color urgent: "red"
|
|
property bool foregroundAnimationEnabled: false
|
|
property bool centerSectionRevealHeld: false
|
|
property bool centerHoverRevealSuppressed: false
|
|
property var shell: mockShell
|
|
function run(command) {
|
|
root.commands.push(String(command))
|
|
}
|
|
function showTooltip(target, text) {}
|
|
function hideTooltip(target) {}
|
|
function registerClickTarget(target) {}
|
|
function unregisterClickTarget(target) {}
|
|
}
|
|
|
|
QtObject {
|
|
id: indicatorHost
|
|
property bool revealInactiveIndicators: true
|
|
signal refreshRequested()
|
|
}
|
|
|
|
function createIndicator(name) {
|
|
var component = Qt.createComponent("file://" + rootPath + "/shell/plugins/bar/indicators/" + name + ".qml")
|
|
if (component.status !== Component.Ready) {
|
|
fail(name + " failed to load: " + component.errorString())
|
|
return null
|
|
}
|
|
|
|
var item = component.createObject(root, {
|
|
indicatorHost: indicatorHost,
|
|
indicatorBlock: "inactive",
|
|
activeOverride: null
|
|
})
|
|
if (!item) {
|
|
fail(name + " failed to instantiate: " + component.errorString())
|
|
return null
|
|
}
|
|
return item
|
|
}
|
|
|
|
function injectBar(item) {
|
|
assertTrue(item.bar === null || item.bar === undefined, item.moduleName + " starts without bar")
|
|
item.bar = mockBar
|
|
assertTrue(item.bar === mockBar, item.moduleName + " accepts delayed bar injection")
|
|
}
|
|
|
|
function commandCount(command) {
|
|
var count = 0
|
|
for (var i = 0; i < commands.length; i++) {
|
|
if (commands[i] === command) count++
|
|
}
|
|
return count
|
|
}
|
|
|
|
function writeResult() {
|
|
var payload = JSON.stringify({
|
|
ok: failures.length === 0,
|
|
failures: failures,
|
|
commands: commands,
|
|
dnd: notificationService.doNotDisturb
|
|
})
|
|
|
|
if (resultPath) {
|
|
Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)])
|
|
}
|
|
}
|
|
|
|
function checkIndicatorTray() {
|
|
idleService.setIdleEnabled(true)
|
|
|
|
var component = Qt.createComponent("file://" + rootPath + "/shell/plugins/bar/widgets/Indicators.qml")
|
|
if (component.status !== Component.Ready) {
|
|
fail("Indicators failed to load: " + component.errorString())
|
|
writeResult()
|
|
return
|
|
}
|
|
|
|
var tray = component.createObject(root, {
|
|
bar: mockBar,
|
|
settings: { items: ["StayAwake"] }
|
|
})
|
|
if (!tray) {
|
|
fail("Indicators failed to instantiate: " + component.errorString())
|
|
writeResult()
|
|
return
|
|
}
|
|
|
|
Qt.callLater(function() {
|
|
root.assertTrue(tray.implicitWidth === 0, "inactive indicator tray starts collapsed")
|
|
mockBar.centerSectionRevealHeld = true
|
|
|
|
Qt.callLater(function() {
|
|
root.assertTrue(tray.implicitWidth > 0, "inactive indicator tray expands on center hover")
|
|
mockBar.centerSectionRevealHeld = false
|
|
|
|
Qt.callLater(function() {
|
|
root.assertTrue(tray.implicitWidth === 0, "inactive indicator tray collapses after hover")
|
|
tray.destroy()
|
|
root.writeResult()
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
function shellQuote(value) {
|
|
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
|
}
|
|
|
|
readonly property string rootPath: Quickshell.env("OMARCHY_PATH")
|
|
|
|
Timer {
|
|
interval: 1
|
|
running: true
|
|
repeat: false
|
|
onTriggered: {
|
|
var dnd = root.createIndicator("Dnd")
|
|
if (dnd) {
|
|
dnd.moduleName = "Dnd"
|
|
root.injectBar(dnd)
|
|
notificationService.doNotDisturb = false
|
|
dnd.triggerPress(Qt.LeftButton)
|
|
root.assertTrue(notificationService.doNotDisturb === true, "DND left click toggles notification service")
|
|
}
|
|
|
|
var nightLight = root.createIndicator("NightLight")
|
|
if (nightLight) {
|
|
nightLight.moduleName = "NightLight"
|
|
root.injectBar(nightLight)
|
|
nightLight.triggerPress(Qt.LeftButton)
|
|
root.assertTrue(nightlightService.enabled === true, "Night Light left click toggles the nightlight service")
|
|
}
|
|
|
|
var screenRecording = root.createIndicator("ScreenRecording")
|
|
if (screenRecording) {
|
|
screenRecording.moduleName = "ScreenRecording"
|
|
root.injectBar(screenRecording)
|
|
screenRecording.triggerPress(Qt.LeftButton)
|
|
root.assertTrue(root.commandCount("omarchy-menu toggle trigger.capture.screenrecord") === 1, "Screen Recording left click opens capture menu when idle")
|
|
screenRecording.recording = true
|
|
screenRecording.triggerPress(Qt.LeftButton)
|
|
root.assertTrue(root.commandCount("omarchy-capture-screenrecording --stop-recording") === 1, "Screen Recording left click stops active recording")
|
|
}
|
|
|
|
var remoteSession = root.createIndicator("RemoteSession")
|
|
if (remoteSession) {
|
|
remoteSession.moduleName = "RemoteSession"
|
|
root.injectBar(remoteSession)
|
|
root.assertTrue(remoteSession.active === false, "Remote Session starts inactive without a gliff server")
|
|
remoteSessionService.active = true
|
|
remoteSessionService.peers = ["10.0.0.5"]
|
|
root.assertTrue(remoteSession.active === true, "Remote Session follows the service state")
|
|
root.assertTrue(remoteSession.tooltipText === "Remote session from 10.0.0.5", "Remote Session tooltip names the connected peer")
|
|
indicatorHost.refreshRequested()
|
|
root.assertTrue(remoteSessionService.refreshes === 1, "Remote Session refresh re-probes the service")
|
|
}
|
|
|
|
var stayAwake = root.createIndicator("StayAwake")
|
|
if (stayAwake) {
|
|
stayAwake.moduleName = "StayAwake"
|
|
root.injectBar(stayAwake)
|
|
stayAwake.triggerPress(Qt.LeftButton)
|
|
root.assertTrue(idleService.stayAwake === true, "Stay Awake left click toggles the idle service")
|
|
}
|
|
|
|
root.checkIndicatorTray()
|
|
}
|
|
}
|
|
}
|