diff --git a/Common/SessionData.qml b/Common/SessionData.qml index b945ed63..03349296 100644 --- a/Common/SessionData.qml +++ b/Common/SessionData.qml @@ -45,6 +45,18 @@ Singleton { property string wallpaperCyclingTime: "06:00" // HH:mm format property string lastBrightnessDevice: "" + // Power management settings - AC Power + property int acMonitorTimeout: 0 // Never + property int acLockTimeout: 0 // Never + property int acSuspendTimeout: 0 // Never + property int acHibernateTimeout: 0 // Never + + // Power management settings - Battery + property int batteryMonitorTimeout: 0 // Never + property int batteryLockTimeout: 0 // Never + property int batterySuspendTimeout: 0 // Never + property int batteryHibernateTimeout: 0 // Never + Component.onCompleted: { loadSettings() } @@ -98,6 +110,15 @@ Singleton { wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300 wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00" lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : "" + + acMonitorTimeout = settings.acMonitorTimeout !== undefined ? settings.acMonitorTimeout : 0 + acLockTimeout = settings.acLockTimeout !== undefined ? settings.acLockTimeout : 0 + acSuspendTimeout = settings.acSuspendTimeout !== undefined ? settings.acSuspendTimeout : 0 + acHibernateTimeout = settings.acHibernateTimeout !== undefined ? settings.acHibernateTimeout : 0 + batteryMonitorTimeout = settings.batteryMonitorTimeout !== undefined ? settings.batteryMonitorTimeout : 0 + batteryLockTimeout = settings.batteryLockTimeout !== undefined ? settings.batteryLockTimeout : 0 + batterySuspendTimeout = settings.batterySuspendTimeout !== undefined ? settings.batterySuspendTimeout : 0 + batteryHibernateTimeout = settings.batteryHibernateTimeout !== undefined ? settings.batteryHibernateTimeout : 0 // Generate system themes but don't override user's theme choice if (typeof Theme !== "undefined") { @@ -138,7 +159,15 @@ Singleton { "wallpaperCyclingMode": wallpaperCyclingMode, "wallpaperCyclingInterval": wallpaperCyclingInterval, "wallpaperCyclingTime": wallpaperCyclingTime, - "lastBrightnessDevice": lastBrightnessDevice + "lastBrightnessDevice": lastBrightnessDevice, + "acMonitorTimeout": acMonitorTimeout, + "acLockTimeout": acLockTimeout, + "acSuspendTimeout": acSuspendTimeout, + "acHibernateTimeout": acHibernateTimeout, + "batteryMonitorTimeout": batteryMonitorTimeout, + "batteryLockTimeout": batteryLockTimeout, + "batterySuspendTimeout": batterySuspendTimeout, + "batteryHibernateTimeout": batteryHibernateTimeout }, null, 2)) } @@ -380,6 +409,46 @@ Singleton { saveSettings() } + function setAcMonitorTimeout(timeout) { + acMonitorTimeout = timeout + saveSettings() + } + + function setAcLockTimeout(timeout) { + acLockTimeout = timeout + saveSettings() + } + + function setAcSuspendTimeout(timeout) { + acSuspendTimeout = timeout + saveSettings() + } + + function setBatteryMonitorTimeout(timeout) { + batteryMonitorTimeout = timeout + saveSettings() + } + + function setBatteryLockTimeout(timeout) { + batteryLockTimeout = timeout + saveSettings() + } + + function setBatterySuspendTimeout(timeout) { + batterySuspendTimeout = timeout + saveSettings() + } + + function setAcHibernateTimeout(timeout) { + acHibernateTimeout = timeout + saveSettings() + } + + function setBatteryHibernateTimeout(timeout) { + batteryHibernateTimeout = timeout + saveSettings() + } + FileView { id: settingsFile diff --git a/Modals/Settings/PowerSettings.qml b/Modals/Settings/PowerSettings.qml new file mode 100644 index 00000000..b7d21410 --- /dev/null +++ b/Modals/Settings/PowerSettings.qml @@ -0,0 +1,237 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: powerTab + + DankFlickable { + anchors.fill: parent + anchors.topMargin: Theme.spacingL + clip: true + contentHeight: mainColumn.height + contentWidth: width + + Column { + id: mainColumn + width: parent.width + spacing: Theme.spacingXL + + StyledText { + text: "Battery not detected - only AC power settings available" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + visible: !BatteryService.batteryAvailable + } + + StyledRect { + width: parent.width + height: timeoutSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Column { + id: timeoutSection + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "schedule" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "Idle Settings" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: Math.max(0, parent.width - parent.children[0].width - parent.children[1].width - powerCategory.width - Theme.spacingM * 3) + height: parent.height + } + + DankButtonGroup { + id: powerCategory + anchors.verticalCenter: parent.verticalCenter + visible: BatteryService.batteryAvailable + model: ["AC Power", "Battery"] + currentIndex: 0 + selectionMode: "single" + } + } + + DankDropdown { + id: lockDropdown + property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"] + property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] + + width: parent.width + text: "Automatically lock after" + options: timeoutOptions + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout + const index = lockDropdown.timeoutValues.indexOf(currentTimeout) + lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never" + } + } + + Component.onCompleted: { + const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acLockTimeout : SessionData.batteryLockTimeout + const index = timeoutValues.indexOf(currentTimeout) + currentValue = index >= 0 ? timeoutOptions[index] : "Never" + } + + onValueChanged: value => { + const index = timeoutOptions.indexOf(value) + if (index >= 0) { + const timeout = timeoutValues[index] + if (powerCategory.currentIndex === 0) { + SessionData.setAcLockTimeout(timeout) + } else { + SessionData.setBatteryLockTimeout(timeout) + } + } + } + } + + DankDropdown { + id: monitorDropdown + property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"] + property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] + + width: parent.width + text: "Turn off monitors after" + options: timeoutOptions + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout + const index = monitorDropdown.timeoutValues.indexOf(currentTimeout) + monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never" + } + } + + Component.onCompleted: { + const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acMonitorTimeout : SessionData.batteryMonitorTimeout + const index = timeoutValues.indexOf(currentTimeout) + currentValue = index >= 0 ? timeoutOptions[index] : "Never" + } + + onValueChanged: value => { + const index = timeoutOptions.indexOf(value) + if (index >= 0) { + const timeout = timeoutValues[index] + if (powerCategory.currentIndex === 0) { + SessionData.setAcMonitorTimeout(timeout) + } else { + SessionData.setBatteryMonitorTimeout(timeout) + } + } + } + } + + DankDropdown { + id: suspendDropdown + property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"] + property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] + + width: parent.width + text: "Suspend system after" + options: timeoutOptions + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout + const index = suspendDropdown.timeoutValues.indexOf(currentTimeout) + suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never" + } + } + + Component.onCompleted: { + const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acSuspendTimeout : SessionData.batterySuspendTimeout + const index = timeoutValues.indexOf(currentTimeout) + currentValue = index >= 0 ? timeoutOptions[index] : "Never" + } + + onValueChanged: value => { + const index = timeoutOptions.indexOf(value) + if (index >= 0) { + const timeout = timeoutValues[index] + if (powerCategory.currentIndex === 0) { + SessionData.setAcSuspendTimeout(timeout) + } else { + SessionData.setBatterySuspendTimeout(timeout) + } + } + } + } + + DankDropdown { + id: hibernateDropdown + property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"] + property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800] + + width: parent.width + text: "Hibernate system after" + description: "Requires swap space or a hibernation file" + options: timeoutOptions + + Connections { + target: powerCategory + function onCurrentIndexChanged() { + const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout + const index = hibernateDropdown.timeoutValues.indexOf(currentTimeout) + hibernateDropdown.currentValue = index >= 0 ? hibernateDropdown.timeoutOptions[index] : "Never" + } + } + + Component.onCompleted: { + const currentTimeout = powerCategory.currentIndex === 0 ? SessionData.acHibernateTimeout : SessionData.batteryHibernateTimeout + const index = timeoutValues.indexOf(currentTimeout) + currentValue = index >= 0 ? timeoutOptions[index] : "Never" + } + + onValueChanged: value => { + const index = timeoutOptions.indexOf(value) + if (index >= 0) { + const timeout = timeoutValues[index] + if (powerCategory.currentIndex === 0) { + SessionData.setAcHibernateTimeout(timeout) + } else { + SessionData.setBatteryHibernateTimeout(timeout) + } + } + } + } + + StyledText { + text: "Idle monitoring not supported - requires newer Quickshell version" + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + anchors.horizontalCenter: parent.horizontalCenter + visible: !IdleService.idleMonitorAvailable + } + } + } + + } + } +} \ No newline at end of file diff --git a/Modals/Settings/SettingsContent.qml b/Modals/Settings/SettingsContent.qml index 886dccea..df4371a4 100644 --- a/Modals/Settings/SettingsContent.qml +++ b/Modals/Settings/SettingsContent.qml @@ -141,13 +141,26 @@ Item { } Loader { - id: aboutLoader + id: powerLoader anchors.fill: parent active: root.currentIndex === 9 visible: active asynchronous: true + sourceComponent: PowerSettings { + } + + } + + Loader { + id: aboutLoader + + anchors.fill: parent + active: root.currentIndex === 10 + visible: active + asynchronous: true + sourceComponent: AboutTab { } diff --git a/Modals/Settings/SettingsSidebar.qml b/Modals/Settings/SettingsSidebar.qml index 6b7660c6..3e39e053 100644 --- a/Modals/Settings/SettingsSidebar.qml +++ b/Modals/Settings/SettingsSidebar.qml @@ -35,6 +35,9 @@ Rectangle { }, { "text": "Theme & Colors", "icon": "palette" + }, { + "text": "Power", + "icon": "power_settings_new" }, { "text": "About", "icon": "info" diff --git a/Modules/Lock/Lock.qml b/Modules/Lock/Lock.qml index dbfd1684..ab515ed6 100644 --- a/Modules/Lock/Lock.qml +++ b/Modules/Lock/Lock.qml @@ -18,6 +18,14 @@ Item { getSessionPath.running = true } + Connections { + target: IdleService + function onLockRequested() { + console.log("Lock: Received lock request from IdleService") + activate() + } + } + Process { id: getSessionPath command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid] diff --git a/README.md b/README.md index 0171161a..a17e36f0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # DankMaterialShell (dms) +*Replace your Waybars, Fuzzels, Swayidles, and Swaylocks with a single Dank shell*
diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 52b90f56..88c34d3b 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -162,6 +162,26 @@ Singleton { } } + function powerOffMonitors() { + if (isNiri) { + return NiriService.powerOffMonitors() + } + if (isHyprland) { + return Hyprland.dispatch("dpms off") + } + console.warn("CompositorService: Cannot power off monitors, unknown compositor") + } + + function powerOnMonitors() { + if (isNiri) { + return NiriService.powerOnMonitors() + } + if (isHyprland) { + return Hyprland.dispatch("dpms on") + } + console.warn("CompositorService: Cannot power on monitors, unknown compositor") + } + Process { id: niriSocketCheck command: ["test", "-S", root.niriSocket] diff --git a/Services/IdleService.qml b/Services/IdleService.qml new file mode 100644 index 00000000..2b4b703c --- /dev/null +++ b/Services/IdleService.qml @@ -0,0 +1,211 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Services + +Singleton { + id: root + + readonly property bool idleMonitorAvailable: { + try { + return typeof IdleMonitor !== "undefined" + } catch (e) { + return false + } + } + + property bool enabled: true + property bool respectInhibitors: true + property bool _enableGate: true + + readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn + readonly property int monitorTimeout: isOnBattery ? SessionData.batteryMonitorTimeout : SessionData.acMonitorTimeout + readonly property int lockTimeout: isOnBattery ? SessionData.batteryLockTimeout : SessionData.acLockTimeout + readonly property int suspendTimeout: isOnBattery ? SessionData.batterySuspendTimeout : SessionData.acSuspendTimeout + readonly property int hibernateTimeout: isOnBattery ? SessionData.batteryHibernateTimeout : SessionData.acHibernateTimeout + + readonly property int firstTimeout: { + const timeouts = [] + if (monitorTimeout > 0) timeouts.push(monitorTimeout) + if (lockTimeout > 0) timeouts.push(lockTimeout) + if (suspendTimeout > 0) timeouts.push(suspendTimeout) + if (hibernateTimeout > 0) timeouts.push(hibernateTimeout) + return timeouts.length > 0 ? Math.min(...timeouts) : 0 + } + + property int currentStepIndex: -1 + property var steps: [] + + signal lockRequested() + signal requestMonitorOff() + signal requestMonitorOn() + signal requestSuspend() + signal requestHibernate() + signal stageFired(string name) + + onFirstTimeoutChanged: { + if (idleMonitor) _rearmIdleMonitor() + } + + function _rearmIdleMonitor() { + cancel() + + _enableGate = false + Qt.callLater(() => { _enableGate = true }) + } + + function makeSteps() { + const steps = [] + if (lockTimeout > 0) { + steps.push({name: "lock", delaySec: lockTimeout}) + } + if (monitorTimeout > 0) { + steps.push({name: "monitor-off", delaySec: monitorTimeout}) + } + if (suspendTimeout > 0) { + steps.push({name: "suspend", delaySec: suspendTimeout}) + } + if (hibernateTimeout > 0) { + steps.push({name: "hibernate", delaySec: hibernateTimeout}) + } + return steps.sort((a, b) => a.delaySec - b.delaySec) + } + + function start() { + if (!enabled || !idleMonitorAvailable) return + if (currentStepIndex !== -1) return + + steps = makeSteps() + currentStepIndex = -1 + next() + } + + function next() { + if (++currentStepIndex >= steps.length) return + + const currentStep = steps[currentStepIndex] + + const firstStepDelay = steps[0].delaySec + const relativeDelay = currentStep.delaySec - firstStepDelay + const ms = (relativeDelay * 1000) | 0 + + if (ms > 0) { + stepTimer.interval = ms + stepTimer.restart() + } else { + Qt.callLater(run) + } + } + + function run() { + const currentStep = steps[currentStepIndex] + if (!currentStep) return + + console.log("IdleService: Executing step:", currentStep.name) + + if (currentStep.name === "lock") { + lockRequested() + } else if (currentStep.name === "monitor-off") { + requestMonitorOff() + } else if (currentStep.name === "suspend") { + requestSuspend() + } else if (currentStep.name === "hibernate") { + requestHibernate() + } + + stageFired(currentStep.name) + next() + } + + function cancel() { + stepTimer.stop() + currentStepIndex = -1 + } + + function wake() { + cancel() + requestMonitorOn() + } + + Timer { + id: stepTimer + repeat: false + onTriggered: root.run() + } + + property var idleMonitor: null + + function createIdleMonitor() { + if (!idleMonitorAvailable) { + console.log("IdleService: IdleMonitor not available, skipping creation") + return + } + + try { + const qmlString = ` + import QtQuick + import Quickshell.Wayland + + IdleMonitor { + enabled: false + respectInhibitors: true + timeout: 0 + } + ` + + idleMonitor = Qt.createQmlObject(qmlString, root, "IdleService.IdleMonitor") + + if (idleMonitor) { + idleMonitor.enabled = Qt.binding( + () => root._enableGate && root.enabled && root.idleMonitorAvailable && root.firstTimeout > 0 + ) + idleMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors) + idleMonitor.timeout = Qt.binding(() => root.firstTimeout) + + idleMonitor.isIdleChanged.connect(function() { + if (idleMonitor.isIdle) { + console.log("IdleService: User is idle, starting power management") + Qt.callLater(root.start) + } else { + console.log("IdleService: User is active, canceling power management") + Qt.callLater(root.cancel) + } + }) + } + } catch (e) { + console.warn("IdleService: Error creating IdleMonitor:", e) + } + } + + Connections { + target: root + function onRequestMonitorOff() { + CompositorService.powerOffMonitors() + } + + function onRequestMonitorOn() { + CompositorService.powerOnMonitors() + } + + function onRequestSuspend() { + SessionService.suspend() + } + + function onRequestHibernate() { + SessionService.hibernate() + } + } + + Component.onCompleted: { + if (!idleMonitorAvailable) { + console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.") + } else { + console.log("IdleService: Initialized with idle monitoring support") + createIdleMonitor() + } + } +} \ No newline at end of file diff --git a/Services/NiriService.qml b/Services/NiriService.qml index bed32ae8..52d64760 100644 --- a/Services/NiriService.qml +++ b/Services/NiriService.qml @@ -438,6 +438,22 @@ Singleton { }) } + function powerOffMonitors() { + return send({ + "Action": { + "PowerOffMonitors": {} + } + }) + } + + function powerOnMonitors() { + return send({ + "Action": { + "PowerOnMonitors": {} + } + }) + } + function getCurrentOutputWorkspaceNumbers() { return currentOutputWorkspaces.map(w => w.idx + 1) } diff --git a/Services/SessionService.qml b/Services/SessionService.qml index bf1ddf73..155b49cd 100644 --- a/Services/SessionService.qml +++ b/Services/SessionService.qml @@ -85,6 +85,10 @@ Singleton { Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"]) } + function hibernate() { + Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "hibernate"]) + } + function reboot() { Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"]) }