diff --git a/bin/omarchy-refresh-hypridle b/bin/omarchy-refresh-hypridle deleted file mode 100755 index 210e78bf..00000000 --- a/bin/omarchy-refresh-hypridle +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -# omarchy:summary=Overwrite the user config for hypridle with the Omarchy default and restart the service. - -omarchy-refresh-config hypr/hypridle.conf -omarchy-restart-hypridle diff --git a/bin/omarchy-restart-hypridle b/bin/omarchy-restart-hypridle deleted file mode 100755 index 0ea7776c..00000000 --- a/bin/omarchy-restart-hypridle +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# omarchy:summary=Restart the hypridle service (used for idle detection and auto-lock). - -omarchy-restart-app hypridle diff --git a/bin/omarchy-toggle-idle b/bin/omarchy-toggle-idle index 9685f047..7e071018 100755 --- a/bin/omarchy-toggle-idle +++ b/bin/omarchy-toggle-idle @@ -1,13 +1,22 @@ #!/bin/bash -# omarchy:summary=Toggle hypridle so the system either idles normally or stays awake +# omarchy:summary=Toggle idle behavior so the system either idles normally or stays awake -if pgrep -x hypridle >/dev/null; then - pkill -x hypridle - omarchy-notification-send -g "󰅶" "Staying awake" "Idle behavior disabled" -else - uwsm-app -- hypridle >/dev/null 2>&1 & - omarchy-notification-send -g "󰾪" "Idle behavior on" "System can now sleep / lock on idle" +if ! state=$(omarchy-shell idle toggle); then + exit 1 fi +case $state in +enabled) + omarchy-notification-send -g "󰾪" "Idle behavior on" "System can now lock on idle" + ;; +disabled) + omarchy-notification-send -g "󰅶" "Staying awake" "Idle behavior disabled" + ;; +*) + echo "Unexpected idle state: $state" >&2 + exit 1 + ;; +esac + omarchy-shell bar refreshIndicators >/dev/null 2>&1 || true diff --git a/config/hypr/hypridle.conf b/config/hypr/hypridle.conf deleted file mode 100644 index 08b27ef8..00000000 --- a/config/hypr/hypridle.conf +++ /dev/null @@ -1,19 +0,0 @@ -general { - lock_cmd = omarchy-system-lock # lock screen and 1password - before_sleep_cmd = OMARCHY_LOCK_ONLY=true omarchy-system-lock # lock before suspend without scheduling display off. - after_sleep_cmd = sleep 1 && omarchy-system-wake # delay for PAM readiness, then turn on display. - inhibit_sleep = 3 # wait until screen is locked -} - -# Start screensaver after 2.5 minutes -listener { - timeout = 150 - on-timeout = pidof hyprlock || omarchy-launch-screensaver -} - -# Lock system after 5 minutes (screensaver resets idle timer, so have to just do half + 2s margin) -listener { - timeout = 152 - on-timeout = omarchy-system-lock - on-resume = omarchy-system-wake -} diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index 29cfd3b7..1d1cb6af 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -4,7 +4,6 @@ hl.on("hyprland.start", function() hl.exec_cmd("dbus-update-activation-environment --systemd --all") hl.exec_cmd("quickshell -n -p $HOME/.local/share/omarchy/shell") - hl.exec_cmd("uwsm-app -- hypridle") hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem") hl.exec_cmd("omarchy-first-run") hl.exec_cmd("omarchy-powerprofiles-init") diff --git a/default/omarchy-skill/SKILL.md b/default/omarchy-skill/SKILL.md index 7d84b279..22c261e2 100644 --- a/default/omarchy-skill/SKILL.md +++ b/default/omarchy-skill/SKILL.md @@ -136,8 +136,7 @@ Run `omarchy --help` for the full list. The most common groups: ├── looknfeel.conf # Appearance (gaps, borders, animations) ├── envs.conf # Environment variables ├── autostart.conf # Startup applications -├── hypridle.conf # Idle behavior (screen off, lock, suspend) -├── hyprlock.conf # Lock screen appearance +├── hyprlock.conf # Lock screen appearance and auth └── hyprsunset.conf # Night light / blue light filter ``` diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 431c9585..905365aa 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -76,7 +76,7 @@ "trigger.share.receive": {"icon":"󰥦","label":"Receive","keywords":"localsend","action":"uwsm-app -- localsend"}, "trigger.toggle.screensaver": {"icon":"󱄄","label":"Screensaver","action":"omarchy-toggle-screensaver"}, "trigger.toggle.nightlight": {"icon":"󰔎","label":"Nightlight","keywords":"night light hyprsunset","action":"omarchy-toggle-nightlight"}, - "trigger.toggle.idle-lock": {"icon":"󱫖","label":"Idle Lock","keywords":"hypridle","action":"omarchy-toggle-idle"}, + "trigger.toggle.idle-lock": {"icon":"󱫖","label":"Idle Lock","keywords":"idle quickshell","action":"omarchy-toggle-idle"}, "trigger.toggle.notifications": {"icon":"󰂛","label":"Notifications","keywords":"silence dnd","action":"omarchy-toggle-notification-silencing"}, "trigger.toggle.top-bar": {"icon":"󰍜","label":"Bar","keywords":"top bar quickshell","action":"omarchy-toggle-bar"}, "trigger.toggle.workspace-layout": {"icon":"󱂬","label":"Workspace Layout","action":"omarchy-hyprland-workspace-layout-toggle"}, @@ -162,7 +162,6 @@ "setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"}, "setup.security.fido2": {"icon":"","label":"Fido2","keywords":"key","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"}, "setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""}, - "setup.config.hypridle": {"icon":"","label":"Hypridle","keywords":"idle","action":"omarchy-launch-config-editor ~/.config/hypr/hypridle.conf && omarchy-restart-hypridle"}, "setup.config.hyprlock": {"icon":"","label":"Hyprlock","keywords":"lock","action":"omarchy-launch-config-editor ~/.config/hypr/hyprlock.conf"}, "setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"night light","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"}, "setup.config.walker": {"icon":"󰌧","label":"Walker","keywords":"launcher","action":"omarchy-launch-config-editor ~/.config/walker/config.toml && omarchy-restart-walker"}, @@ -309,13 +308,11 @@ "update.channel.rc": {"icon":"🟡","label":"RC","keywords":"release candidate","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set rc'"}, "update.channel.edge": {"icon":"🟠","label":"Edge","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set edge'"}, "update.channel.dev": {"icon":"🔴","label":"Dev","keywords":"development","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-channel-set dev'"}, - "update.process.hypridle": {"icon":"","label":"Hypridle","keywords":"restart","action":"omarchy-restart-hypridle"}, "update.process.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"restart","action":"omarchy-restart-hyprsunset"}, "update.process.osd": {"icon":"","label":"OSD","keywords":"restart","action":"omarchy-restart-shell"}, "update.process.walker": {"icon":"󰌧","label":"Walker","keywords":"restart launcher","action":"omarchy-restart-walker"}, "update.process.shell": {"icon":"󰍜","label":"Shell","keywords":"restart bar quickshell omarchy-shell","action":"omarchy-restart-shell"}, "update.config.hyprland": {"icon":"","label":"Hyprland","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprland"}, - "update.config.hypridle": {"icon":"","label":"Hypridle","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hypridle"}, "update.config.hyprlock": {"icon":"","label":"Hyprlock","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprlock"}, "update.config.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprsunset"}, "update.config.plymouth": {"icon":"󱣴","label":"Plymouth","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-plymouth"}, diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index e7aa909a..3849921c 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -67,6 +67,10 @@ individual plugins (`bar`, `image-selector`, …). ```json { "version": 1, + "idle": { + "screensaver": 150, + "lock": 300 + }, "bar": { "position": "top", "transparent": false, @@ -92,7 +96,8 @@ Rules: merge layers. 3. Third-party enabled ⇔ present; first-party plugins are always enabled. 4. `allowMultiple: true` in the manifest permits multiple instances. -5. `version: 1` is required. +5. `idle.screensaver` and `idle.lock` are seconds since user idle began. +6. `version: 1` is required. `shell-defaults.json` describes the fresh-install state. When no user `shell.json` exists, defaults are used verbatim. Once the user diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index d6dca50d..50306221 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -49,7 +49,6 @@ gum gvfs-mtp gvfs-nfs gvfs-smb -hypridle hyprland hyprland-guiutils hyprland-preview-share-picker diff --git a/migrations/1778054328.sh b/migrations/1778054328.sh deleted file mode 100644 index 1722c7b4..00000000 --- a/migrations/1778054328.sh +++ /dev/null @@ -1,3 +0,0 @@ -echo "Use omarchy-system-lock and omarchy-system-wake in hypridle" - -omarchy-refresh-hypridle diff --git a/migrations/1779182943.sh b/migrations/1779182943.sh new file mode 100644 index 00000000..da3cf09c --- /dev/null +++ b/migrations/1779182943.sh @@ -0,0 +1,10 @@ +echo "Move idle handling from hypridle to omarchy-shell" + +if [[ -f ~/.config/hypr/autostart.lua ]]; then + sed -i '/hypridle/d' ~/.config/hypr/autostart.lua +fi + +omarchy-refresh-shell +pkill -x hypridle 2>/dev/null || true +omarchy-pkg-drop hypridle +omarchy-restart-shell || true diff --git a/shell/README.md b/shell/README.md index c1bf3bf3..97e22384 100644 --- a/shell/README.md +++ b/shell/README.md @@ -160,6 +160,10 @@ rewrites the `bar` subtree from the current `shell-defaults.json`. ```json { "version": 1, + "idle": { + "screensaver": 150, + "lock": 300 + }, "bar": { "position": "top", "transparent": false, @@ -193,7 +197,10 @@ rewrites the `bar` subtree from the current `shell-defaults.json`. `allowMultiple: true`. Each instance is independent — e.g. two clocks in different timezones are just two `{"id":"calendar", "timezone": ...}` entries with their own values. -5. **`version: 1` is required** at the top level. The shell will fall back +5. **Idle timings are top-level.** `idle.screensaver` and `idle.lock` + are seconds since user idle began, so the default lock fires at 300s + even if the 150s screensaver starts first. +6. **`version: 1` is required** at the top level. The shell will fall back to defaults rather than load an unknown version. ## Implementation history diff --git a/shell/plugins/README.md b/shell/plugins/README.md index aa9dee43..26599d13 100644 --- a/shell/plugins/README.md +++ b/shell/plugins/README.md @@ -18,6 +18,7 @@ User-installed plugins live alongside these conceptually but on disk under | Clipboard mgr | `omarchy.clipboard-picker`| `overlay` | `clipboard-picker/ClipboardPicker.qml`| | Omarchy menu | `omarchy.menu` | `menu` | `menu/Menu.qml` | | Notifications | `omarchy.notifications` | `service` | `notifications/Service.qml` | +| Idle monitor | `omarchy.idle` | `service` | `idle/Service.qml` | | OSD | `omarchy.osd` | `panel` | `osd/Osd.qml` | | Polkit agent | `omarchy.polkit` | `service` | `polkit/PolkitAgent.qml` | diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 4a19c136..5628924d 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -12,8 +12,7 @@ import qs.Ui Item { id: root - // The omarchy-shell host injects omarchyPath when it instantiates this Bar. - // Default fallback keeps the file loadable in isolation (e.g. for QML tooling). + // The omarchy-shell host injects omarchyPath from OMARCHY_PATH. required property string omarchyPath // Injected by the host shell. Shared with the bar settings panel so both // see the same widget catalogue. @@ -110,10 +109,6 @@ Item { return "file://" + path.split("/").map(encodeURIComponent).join("/") } - function commandWithOmarchyPath(command) { - return "OMARCHY_PATH=" + shellQuote(omarchyPath) + " " + command - } - function isPlainObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value) } @@ -539,7 +534,7 @@ Item { Process { id: updateProc - command: ["bash", "-lc", root.commandWithOmarchyPath("omarchy-update-available")] + command: ["bash", "-lc", "omarchy-update-available"] stdout: StdioCollector { waitForEnd: true } onExited: function(exitCode) { root.updateAvailable = exitCode === 0 @@ -556,7 +551,7 @@ Item { Process { id: voxtypeProc - command: ["bash", "-lc", root.commandWithOmarchyPath("omarchy-voxtype-status")] + command: ["bash", "-lc", "omarchy-voxtype-status"] running: true stdout: SplitParser { onRead: function(data) { @@ -567,7 +562,7 @@ Item { Process { id: screenRecordingProc - command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/shell/scripts/indicators/screen-recording.sh"))] + command: ["bash", "-lc", root.shellQuote(root.omarchyPath + "/shell/scripts/indicators/screen-recording.sh")] stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateIndicator("screenRecording", text) @@ -576,7 +571,7 @@ Item { Process { id: notificationSilencingProc - command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/shell/scripts/indicators/notification-silencing.sh"))] + command: ["bash", "-lc", root.shellQuote(root.omarchyPath + "/shell/scripts/indicators/notification-silencing.sh")] stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateIndicator("notifications", text) @@ -1037,7 +1032,7 @@ Item { Process { id: customProc - command: ["bash", "-lc", root.commandWithOmarchyPath(String(customRoot.setting("exec", "")))] + command: ["bash", "-lc", String(customRoot.setting("exec", ""))] stdout: StdioCollector { waitForEnd: true onStreamFinished: customRoot.update(text) diff --git a/shell/plugins/bar/widgets/idleInhibitor.qml b/shell/plugins/bar/widgets/idleInhibitor.qml index 2d6b29c8..8017bcab 100644 --- a/shell/plugins/bar/widgets/idleInhibitor.qml +++ b/shell/plugins/bar/widgets/idleInhibitor.qml @@ -33,11 +33,17 @@ Item { Process { id: statusProc - command: ["bash", "-lc", "pgrep -x hypridle >/dev/null 2>&1 && echo running || echo stopped"] + command: ["bash", "-lc", "omarchy-shell idle status 2>/dev/null"] stdout: StdioCollector { waitForEnd: true onStreamFinished: { - root.active = String(text || "").trim() === "stopped" + var raw = String(text || "").trim() + try { + var parsed = JSON.parse(raw) + root.active = parsed && parsed.enabled === false + } catch (e) { + root.active = false + } } } } @@ -65,7 +71,7 @@ Item { bar: root.bar text: root.icon active: false - tooltipText: root.active ? "No sleep, lock, or screensaver" : "" + tooltipText: root.active ? "No lock or screensaver" : "" onPressed: function() { root.toggle() } } } diff --git a/shell/plugins/bar/widgets/networkPanel.qml b/shell/plugins/bar/widgets/networkPanel.qml index 7f771f65..90dfa962 100644 --- a/shell/plugins/bar/widgets/networkPanel.qml +++ b/shell/plugins/bar/widgets/networkPanel.qml @@ -1328,7 +1328,7 @@ iwctl known-networks list 2>/dev/null \\ // Bar.qml does not need to mirror network state. Process { id: networkProc - command: ["bash", "-lc", root.bar ? root.bar.commandWithOmarchyPath(root.networkCommand()) : ""] + command: ["bash", "-lc", root.bar ? root.networkCommand() : ""] stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateNetwork(text) diff --git a/shell/plugins/bar/widgets/weather.qml b/shell/plugins/bar/widgets/weather.qml index a475c4ad..4450fd8d 100644 --- a/shell/plugins/bar/widgets/weather.qml +++ b/shell/plugins/bar/widgets/weather.qml @@ -552,7 +552,7 @@ Item { // Poll the weather pill text/class every minute. Local to this widget. Process { id: weatherProc - command: ["bash", "-lc", root.bar ? root.bar.commandWithOmarchyPath(root.bar.shellQuote(root.bar.omarchyPath + "/shell/scripts/weather.sh")) : ""] + command: ["bash", "-lc", root.bar ? root.bar.shellQuote(root.bar.omarchyPath + "/shell/scripts/weather.sh") : ""] stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateWeather(text) diff --git a/shell/plugins/emoji-picker/EmojiPicker.qml b/shell/plugins/emoji-picker/EmojiPicker.qml index 005fb8ed..136c25a5 100644 --- a/shell/plugins/emoji-picker/EmojiPicker.qml +++ b/shell/plugins/emoji-picker/EmojiPicker.qml @@ -7,7 +7,7 @@ import qs.Commons Item { id: root - property string omarchyPath: Quickshell.env("OMARCHY_PATH") || (Quickshell.env("HOME") + "/.local/share/omarchy") + property string omarchyPath: Quickshell.env("OMARCHY_PATH") property var shell: null property var manifest: null diff --git a/shell/plugins/idle/Service.qml b/shell/plugins/idle/Service.qml new file mode 100644 index 00000000..52e36ed3 --- /dev/null +++ b/shell/plugins/idle/Service.qml @@ -0,0 +1,234 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland + +Item { + id: root + + // Injected by omarchy-shell (the first-party service loader). + property var shell: null + + readonly property int defaultScreensaverSeconds: 150 + readonly property int defaultLockSeconds: 300 + readonly property var idleConfig: shell && shell.shellConfig && shell.shellConfig.idle ? shell.shellConfig.idle : ({}) + readonly property int screensaverTimeoutSeconds: secondsFromConfig(idleConfig.screensaver, defaultScreensaverSeconds) + readonly property int lockTimeoutSeconds: secondsFromConfig(idleConfig.lock, defaultLockSeconds) + readonly property int firstIdleTimeoutSeconds: Math.min(screensaverTimeoutSeconds, lockTimeoutSeconds) + readonly property int screensaverDelaySeconds: Math.max(0, screensaverTimeoutSeconds - firstIdleTimeoutSeconds) + readonly property int lockDelaySeconds: Math.max(0, lockTimeoutSeconds - firstIdleTimeoutSeconds) + readonly property bool idleEnabled: persisted.idleEnabled + + property bool idledThisCycle: false + property bool screensaverStartedThisCycle: false + + PersistentProperties { + id: persisted + reloadableId: "omarchy-idle" + property bool idleEnabled: true + } + + function secondsFromConfig(value, fallback) { + var n = Number(value) + if (!isFinite(n) || n < 0) return fallback + return Math.floor(n) + } + + function runProcess(process, command) { + if (process.running) return false + process.command = ["bash", "-lc", command] + process.running = true + return true + } + + function launchScreensaver() { + root.screensaverStartedThisCycle = true + runProcess(screensaverProcess, "pidof hyprlock >/dev/null || omarchy-launch-screensaver") + } + + function lockSystem() { + screensaverTimer.stop() + lockTimer.stop() + screensaverResumePollTimer.stop() + root.idledThisCycle = false + root.screensaverStartedThisCycle = false + runProcess(lockProcess, "omarchy-system-lock") + } + + function startIdleCycle() { + if (root.idledThisCycle) return + + root.idledThisCycle = true + root.screensaverStartedThisCycle = false + screensaverResumePollTimer.stop() + + if (root.screensaverDelaySeconds === 0) launchScreensaver() + else screensaverTimer.restart() + + if (root.lockDelaySeconds === 0) lockSystem() + else lockTimer.restart() + } + + function cancelIdleCycle() { + screensaverTimer.stop() + lockTimer.stop() + screensaverResumePollTimer.stop() + + if (root.idledThisCycle) runProcess(wakeProcess, "omarchy-system-wake") + + root.idledThisCycle = false + root.screensaverStartedThisCycle = false + } + + function checkScreensaverAfterActiveSignal() { + if (!root.idledThisCycle || !root.screensaverStartedThisCycle) return + if (screensaverCheckProcess.running) return + screensaverCheckProcess.command = ["bash", "-lc", "pgrep -f org.omarchy.screensaver >/dev/null && echo running || echo stopped"] + screensaverCheckProcess.running = true + } + + function handleActiveSignal() { + if (!root.idledThisCycle) return + + // Starting the screensaver can make the compositor report activity. Keep + // the lock timer running while the screensaver is still alive so the lock + // deadline remains `idle.lock` seconds from the original user idle time. + if (root.screensaverStartedThisCycle) { + screensaverResumePollTimer.restart() + return + } + + cancelIdleCycle() + } + + function handleIdleChanged() { + if (!root.idleEnabled) return + + if (idleMonitor.isIdle) startIdleCycle() + else handleActiveSignal() + } + + function statusJson() { + return JSON.stringify({ + enabled: root.idleEnabled, + idle: idleMonitor.isIdle, + inIdleCycle: root.idledThisCycle, + sleepMonitor: sleepMonitorProcess.running, + screensaver: root.screensaverTimeoutSeconds, + lock: root.lockTimeoutSeconds + }) + } + + function handleSleepPreparing(preparing) { + if (!root.idleEnabled) return + + if (preparing) { + cancelIdleCycle() + runProcess(sleepLockProcess, "OMARCHY_LOCK_ONLY=true omarchy-system-lock") + } else { + runProcess(sleepWakeProcess, "sleep 1 && omarchy-system-wake") + } + } + + function setIdleEnabled(value) { + var enabled = !!value + if (persisted.idleEnabled === enabled) return enabled ? "enabled" : "disabled" + + persisted.idleEnabled = enabled + if (!enabled) cancelIdleCycle() + else Qt.callLater(root.handleIdleChanged) + + return enabled ? "enabled" : "disabled" + } + + IdleMonitor { + id: idleMonitor + enabled: root.idleEnabled + timeout: root.firstIdleTimeoutSeconds + respectInhibitors: true + onIsIdleChanged: root.handleIdleChanged() + } + + Timer { + id: screensaverTimer + interval: root.screensaverDelaySeconds * 1000 + repeat: false + onTriggered: root.launchScreensaver() + } + + Timer { + id: lockTimer + interval: root.lockDelaySeconds * 1000 + repeat: false + onTriggered: if (root.idleEnabled && root.idledThisCycle) root.lockSystem() + } + + Timer { + id: screensaverResumePollTimer + interval: 1000 + repeat: true + onTriggered: root.checkScreensaverAfterActiveSignal() + } + + Process { id: screensaverProcess } + Process { id: lockProcess } + Process { id: wakeProcess } + Process { id: sleepLockProcess } + Process { id: sleepWakeProcess } + + Process { + id: screensaverCheckProcess + stdout: StdioCollector { + id: screensaverCheckStdout + waitForEnd: true + } + onExited: { + if (!root.idledThisCycle || !root.screensaverStartedThisCycle) return + if (String(screensaverCheckStdout.text || "").trim() === "running") return + root.cancelIdleCycle() + } + } + + Process { + id: sleepMonitorProcess + command: ["bash", "-lc", "exec dbus-monitor --system \"type='signal',sender='org.freedesktop.login1',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'\""] + running: true + stdout: SplitParser { + onRead: function(line) { + var text = String(line || "").trim() + if (text === "boolean true") root.handleSleepPreparing(true) + else if (text === "boolean false") root.handleSleepPreparing(false) + } + } + onExited: sleepMonitorRestartTimer.restart() + } + + Timer { + id: sleepMonitorRestartTimer + interval: 5000 + repeat: false + onTriggered: if (!sleepMonitorProcess.running) sleepMonitorProcess.running = true + } + + Component.onCompleted: Qt.callLater(root.handleIdleChanged) + + IpcHandler { + target: "idle" + + function status(): string { + return root.statusJson() + } + + function enable(): string { + return root.setIdleEnabled(true) + } + + function disable(): string { + return root.setIdleEnabled(false) + } + + function toggle(): string { + return root.setIdleEnabled(!root.idleEnabled) + } + } +} diff --git a/shell/plugins/idle/manifest.json b/shell/plugins/idle/manifest.json new file mode 100644 index 00000000..83ce7fac --- /dev/null +++ b/shell/plugins/idle/manifest.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 1, + "id": "omarchy.idle", + "name": "Idle Monitor", + "version": "1.0.0", + "author": "Omarchy", + "description": "Quickshell-based idle detection for screensaver, lock, and display wake.", + "kinds": [ + "service" + ], + "keepLoaded": true, + "entryPoints": { + "service": "Service.qml" + } +} diff --git a/shell/plugins/image-picker/ImagePicker.qml b/shell/plugins/image-picker/ImagePicker.qml index 5754ea4b..45708be1 100644 --- a/shell/plugins/image-picker/ImagePicker.qml +++ b/shell/plugins/image-picker/ImagePicker.qml @@ -9,9 +9,8 @@ import qs.Commons Item { id: root - // Injected by omarchy-shell so helper scripts resolve without relying on - // OMARCHY_PATH being set in the environment. - property string omarchyPath: "" + // Injected by omarchy-shell; defaults to the session OMARCHY_PATH. + property string omarchyPath: Quickshell.env("OMARCHY_PATH") property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds") property string imageRows: "" property string loadedImageRows: "" @@ -53,8 +52,7 @@ Item { } function scriptPath(name) { - var base = omarchyPath || Quickshell.env("OMARCHY_PATH") || (Quickshell.env("HOME") + "/.local/share/omarchy") - return base + "/shell/scripts/" + name + return omarchyPath + "/shell/scripts/" + name } function focusPicker() { diff --git a/shell/plugins/menu/Menu.qml b/shell/plugins/menu/Menu.qml index 507cac0e..8ffe41dd 100644 --- a/shell/plugins/menu/Menu.qml +++ b/shell/plugins/menu/Menu.qml @@ -8,7 +8,7 @@ Item { id: root // Injected by omarchy-shell when this plugin is summoned. - property string omarchyPath: Quickshell.env("OMARCHY_PATH") || (Quickshell.env("HOME") + "/.local/share/omarchy") + property string omarchyPath: Quickshell.env("OMARCHY_PATH") // Plugin lifecycle hooks. The host calls open(payloadJson) after // `omarchy-shell shell summon omarchy.menu ...` and close() when hidden. diff --git a/shell/plugins/settings/SettingsPanel.qml b/shell/plugins/settings/SettingsPanel.qml index d137679f..c123f593 100644 --- a/shell/plugins/settings/SettingsPanel.qml +++ b/shell/plugins/settings/SettingsPanel.qml @@ -33,17 +33,7 @@ Item { property var shell: null // ---------------- paths -------------------------------------------------- - property string omarchyPath: { - var env = Quickshell.env("OMARCHY_PATH") - if (env) return env - var dir = String(Quickshell.shellDir || "") - while (dir.length > 1 && dir.charAt(dir.length - 1) === "/") - dir = dir.substring(0, dir.length - 1) - var suffix = "/shell" - if (dir.length > suffix.length && dir.substring(dir.length - suffix.length) === suffix) - return dir.substring(0, dir.length - suffix.length) - return Quickshell.env("HOME") + "/.local/share/omarchy" - } + property string omarchyPath: Quickshell.env("OMARCHY_PATH") readonly property string home: Quickshell.env("HOME") readonly property string userConfigPath: home + "/.config/omarchy/shell.json" readonly property string defaultsPath: omarchyPath + "/shell/shell-defaults.json" diff --git a/shell/shell-defaults.json b/shell/shell-defaults.json index f6994ff7..d06612b5 100644 --- a/shell/shell-defaults.json +++ b/shell/shell-defaults.json @@ -1,5 +1,9 @@ { "version": 1, + "idle": { + "screensaver": 150, + "lock": 300 + }, "bar": { "position": "top", "transparent": false, diff --git a/shell/shell.qml b/shell/shell.qml index c2c7e4ea..ec8f220f 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -21,21 +21,9 @@ ShellRoot { property string home: Quickshell.env("HOME") // The omarchy-shell host is the long-running entry point. Plugins live in - // sibling directories under plugins/. Most child code (the bar, future - // plugins) accept omarchyPath as a required property; resolve it once here - // from the shellDir so we don't depend on OMARCHY_PATH being set in the env. - function deriveOmarchyPath() { - var env = Quickshell.env("OMARCHY_PATH") - if (env) return env - var dir = String(Quickshell.shellDir || "") - while (dir.length > 1 && dir.charAt(dir.length - 1) === "/") - dir = dir.substring(0, dir.length - 1) - var suffix = "/shell" - if (dir.length > suffix.length && dir.substring(dir.length - suffix.length) === suffix) - return dir.substring(0, dir.length - suffix.length) - return home + "/.local/share/omarchy" - } - property string omarchyPath: deriveOmarchyPath() + // sibling directories under plugins/. OMARCHY_PATH is provided by the uwsm + // session environment and is the single source of truth for this checkout. + property string omarchyPath: Quickshell.env("OMARCHY_PATH") readonly property string shellPath: omarchyPath + "/shell" readonly property string firstPartyPluginsDir: shellPath + "/plugins" readonly property string defaultsPath: shellPath + "/shell-defaults.json" @@ -46,6 +34,10 @@ ShellRoot { // closely enough to render a usable bar; not authoritative. readonly property var builtinShellConfig: ({ version: 1, + idle: { + screensaver: 150, + lock: 300 + }, bar: { position: "top", transparent: false,