1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

Implement IdleMonitor to replace swayidle/hypridle functionality

This commit is contained in:
bbedward
2025-09-19 15:59:40 -04:00
parent e6265c2f71
commit 691b6da7a7
10 changed files with 584 additions and 2 deletions

View File

@@ -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()
}
@@ -99,6 +111,15 @@ Singleton {
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") {
Theme.generateSystemThemesFromCurrentTheme()
@@ -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

View File

@@ -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
}
}
}
}
}
}

View File

@@ -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 {
}

View File

@@ -35,6 +35,9 @@ Rectangle {
}, {
"text": "Theme & Colors",
"icon": "palette"
}, {
"text": "Power",
"icon": "power_settings_new"
}, {
"text": "About",
"icon": "info"

View File

@@ -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]

View File

@@ -1,4 +1,5 @@
# DankMaterialShell (dms)
*Replace your Waybars, Fuzzels, Swayidles, and Swaylocks with a single Dank shell*
<div align=center>

View File

@@ -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]

211
Services/IdleService.qml Normal file
View File

@@ -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()
}
}
}

View File

@@ -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)
}

View File

@@ -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"])
}