diff --git a/Commons/Settings.qml b/Commons/Settings.qml index d7fec7d..3b9466a 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -229,6 +229,9 @@ Singleton { // Legacy compatibility property string fontFamily: fontDefault // Keep for backward compatibility + + // Idle inhibitor state + property bool idleInhibitorEnabled: false } // Scaling (not stored inside JsonObject, or it crashes) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 638bc8e..6eb0d50 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -27,7 +27,8 @@ Item { IpcHandler { target: "idleInhibitor" - function toggle() {// TODO + function toggle() { + return IdleInhibitorService.manualToggle() } } diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 9bc0739..1261c49 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -31,6 +31,17 @@ NBox { } } + // Idle Inhibitor + NIconButton { + icon: IdleInhibitorService.isInhibited ? "coffee" : "bedtime" + tooltipText: IdleInhibitorService.isInhibited ? "Disable Keep Awake" : "Enable Keep Awake" + colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant + colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary + onClicked: { + IdleInhibitorService.manualToggle() + } + } + // Wallpaper NIconButton { icon: "image" diff --git a/Services/IdleInhibitorService.qml b/Services/IdleInhibitorService.qml new file mode 100644 index 0000000..bece9db --- /dev/null +++ b/Services/IdleInhibitorService.qml @@ -0,0 +1,209 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services + +Singleton { + id: root + + property bool isInhibited: false + property string reason: "User requested" + property var activeInhibitors: [] + + // Different inhibitor strategies + property string strategy: "systemd" // "systemd", "wayland", or "auto" + + Component.onCompleted: { + Logger.log("IdleInhibitor", "Service started") + detectStrategy() + + // Restore previous state from settings + if (Settings.data.ui.idleInhibitorEnabled) { + addInhibitor("manual", "Restored from previous session") + Logger.log("IdleInhibitor", "Restored previous manual inhibition state") + } + } + + // Auto-detect the best strategy + function detectStrategy() { + if (strategy === "auto") { + // Check if systemd-inhibit is available + try { + var systemdResult = Quickshell.execDetached(["which", "systemd-inhibit"]) + strategy = "systemd" + Logger.log("IdleInhibitor", "Using systemd-inhibit strategy") + return + } catch (e) { + // systemd-inhibit not found, try Wayland tools + } + + try { + var waylandResult = Quickshell.execDetached(["which", "wayhibitor"]) + strategy = "wayland" + Logger.log("IdleInhibitor", "Using wayhibitor strategy") + return + } catch (e) { + // wayhibitor not found + } + + Logger.warn("IdleInhibitor", "No suitable inhibitor found - will try systemd as fallback") + strategy = "systemd" // Fallback to systemd even if not detected + } + } + + // Add an inhibitor + function addInhibitor(id, reason = "Application request") { + if (activeInhibitors.includes(id)) { + Logger.warn("IdleInhibitor", "Inhibitor already active:", id) + return false + } + + activeInhibitors.push(id) + updateInhibition(reason) + Logger.log("IdleInhibitor", "Added inhibitor:", id) + return true + } + + // Remove an inhibitor + function removeInhibitor(id) { + const index = activeInhibitors.indexOf(id) + if (index === -1) { + Logger.warn("IdleInhibitor", "Inhibitor not found:", id) + return false + } + + activeInhibitors.splice(index, 1) + updateInhibition() + Logger.log("IdleInhibitor", "Removed inhibitor:", id) + return true + } + + // Update the actual system inhibition + function updateInhibition(newReason = reason) { + const shouldInhibit = activeInhibitors.length > 0 + + if (shouldInhibit === isInhibited) { + return // No change needed + } + + if (shouldInhibit) { + startInhibition(newReason) + } else { + stopInhibition() + } + } + + // Start system inhibition + function startInhibition(newReason) { + reason = newReason + + if (strategy === "systemd") { + startSystemdInhibition() + } else if (strategy === "wayland") { + startWaylandInhibition() + } else { + Logger.warn("IdleInhibitor", "No inhibition strategy available") + return + } + + isInhibited = true + Logger.log("IdleInhibitor", "Started inhibition:", reason) + } + + // Stop system inhibition + function stopInhibition() { + if (!isInhibited) return + + if (inhibitorProcess.running) { + inhibitorProcess.signal(15) // SIGTERM + } + + isInhibited = false + Logger.log("IdleInhibitor", "Stopped inhibition") + } + + // Systemd inhibition using systemd-inhibit + function startSystemdInhibition() { + inhibitorProcess.command = [ + "systemd-inhibit", + "--what=idle:sleep:handle-lid-switch", + "--why=" + reason, + "--mode=block", + "sleep", "infinity" + ] + inhibitorProcess.running = true + } + + // Wayland inhibition using wayhibitor or similar + function startWaylandInhibition() { + inhibitorProcess.command = ["wayhibitor"] + inhibitorProcess.running = true + } + + // Process for maintaining the inhibition + Process { + id: inhibitorProcess + running: false + + onExited: function(exitCode, exitStatus) { + if (isInhibited) { + Logger.warn("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode) + isInhibited = false + } + } + + onStarted: function() { + Logger.log("IdleInhibitor", "Inhibitor process started successfully") + } + } + + // Convenience functions for common use cases + function inhibitForMedia(active = true) { + if (active) { + addInhibitor("media", "Media playback active") + } else { + removeInhibitor("media") + } + } + + function inhibitForPresentation(active = true) { + if (active) { + addInhibitor("presentation", "Presentation mode") + } else { + removeInhibitor("presentation") + } + } + + function inhibitForFullscreen(active = true) { + if (active) { + addInhibitor("fullscreen", "Fullscreen application") + } else { + removeInhibitor("fullscreen") + } + } + + // Manual toggle for user control + function manualToggle() { + if (activeInhibitors.includes("manual")) { + removeInhibitor("manual") + Settings.data.ui.idleInhibitorEnabled = false + ToastService.showNotice("Keep Awake", "Disabled", false, 3000) + Logger.log("IdleInhibitor", "Manual inhibition disabled and saved to settings") + return false + } else { + addInhibitor("manual", "Manually activated by user") + Settings.data.ui.idleInhibitorEnabled = true + ToastService.showNotice("Keep Awake", "Enabled", false, 3000) + Logger.log("IdleInhibitor", "Manual inhibition enabled and saved to settings") + return true + } + } + + // Clean up on shutdown + Component.onDestruction: { + stopInhibition() + } +} diff --git a/Services/MediaService.qml b/Services/MediaService.qml index c44738d..b4434a2 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -13,6 +13,11 @@ Singleton { property real currentPosition: 0 property int selectedPlayerIndex: 0 property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false + + // Auto-inhibit idle when media is playing + onIsPlayingChanged: { + IdleInhibitorService.inhibitForMedia(isPlaying) + } property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "") : "" property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" diff --git a/shell.qml b/shell.qml index de85aee..c4bf5e9 100644 --- a/shell.qml +++ b/shell.qml @@ -25,6 +25,7 @@ import qs.Modules.Notification import qs.Modules.SettingsPanel import qs.Modules.SidePanel import qs.Modules.Toast + import qs.Services import qs.Widgets