idle: added support for custom commands

This commit is contained in:
Lemmy
2026-02-22 21:30:28 -05:00
parent 90ae42bda2
commit a12de93d40
11 changed files with 533 additions and 215 deletions
+12 -4
View File
@@ -1193,16 +1193,24 @@
"enable-label": "Enable idle management", "enable-label": "Enable idle management",
"fade-duration-description": "Seconds for the fade-to-black animation before each action fires. Any mouse movement cancels the fade.", "fade-duration-description": "Seconds for the fade-to-black animation before each action fires. Any mouse movement cancels the fade.",
"fade-duration-label": "Fade duration", "fade-duration-label": "Fade duration",
"lock-description": "Minutes of inactivity before the lock screen activates.", "lock-description": "Seconds of inactivity before the lock screen activates.",
"lock-label": "Lock screen", "lock-label": "Lock screen",
"screen-off-description": "Minutes of inactivity before monitors are turned off.", "screen-off-description": "Seconds of inactivity before monitors are turned off.",
"screen-off-label": "Turn off screen", "screen-off-label": "Turn off screen",
"status-description": "Idle time as reported by the compositor.", "status-description": "Idle time as reported by the compositor.",
"status-label": "Idle time", "status-label": "Idle time",
"suspend-description": "Minutes of inactivity before the system suspends.", "suspend-description": "Seconds of inactivity before the system suspends.",
"timeouts-description": "Set to 0 to disable a stage. Timeouts are paused while Keep Awake is active.", "timeouts-description": "Set to 0 to disable a stage. Timeouts are paused while Keep Awake is active.",
"timeouts-label": "Timeouts", "timeouts-label": "Timeouts",
"unavailable": "Native idle monitoring is not available on this compositor." "unavailable": "Native idle monitoring is not available on this compositor.",
"tab-behavior": "Behavior",
"tab-custom": "Custom",
"custom-label": "Custom idle commands",
"custom-description": "Run a shell command after a period of inactivity.",
"custom-add": "Add command",
"custom-entry-timeout": "Inactivity time",
"custom-entry-command": "Command",
"custom-entry-delete": "Delete"
}, },
"indicator": { "indicator": {
"default-value": "Default: {value}", "default-value": "Default: {value}",
+41 -7
View File
@@ -932,7 +932,8 @@
"widget": "NToggle", "widget": "NToggle",
"tab": 13, "tab": 13,
"tabLabel": "common.idle", "tabLabel": "common.idle",
"subTab": null "subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
}, },
{ {
"labelKey": "panels.idle.status-label", "labelKey": "panels.idle.status-label",
@@ -940,7 +941,8 @@
"widget": "NLabel", "widget": "NLabel",
"tab": 13, "tab": 13,
"tabLabel": "common.idle", "tabLabel": "common.idle",
"subTab": null "subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
}, },
{ {
"labelKey": "panels.idle.timeouts-label", "labelKey": "panels.idle.timeouts-label",
@@ -948,7 +950,8 @@
"widget": "NLabel", "widget": "NLabel",
"tab": 13, "tab": 13,
"tabLabel": "common.idle", "tabLabel": "common.idle",
"subTab": null "subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
}, },
{ {
"labelKey": "panels.idle.screen-off-label", "labelKey": "panels.idle.screen-off-label",
@@ -956,7 +959,8 @@
"widget": "NSpinBox", "widget": "NSpinBox",
"tab": 13, "tab": 13,
"tabLabel": "common.idle", "tabLabel": "common.idle",
"subTab": null "subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
}, },
{ {
"labelKey": "panels.idle.lock-label", "labelKey": "panels.idle.lock-label",
@@ -964,7 +968,8 @@
"widget": "NSpinBox", "widget": "NSpinBox",
"tab": 13, "tab": 13,
"tabLabel": "common.idle", "tabLabel": "common.idle",
"subTab": null "subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
}, },
{ {
"labelKey": "common.suspend", "labelKey": "common.suspend",
@@ -972,7 +977,8 @@
"widget": "NSpinBox", "widget": "NSpinBox",
"tab": 13, "tab": 13,
"tabLabel": "common.idle", "tabLabel": "common.idle",
"subTab": null "subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
}, },
{ {
"labelKey": "panels.idle.fade-duration-label", "labelKey": "panels.idle.fade-duration-label",
@@ -980,7 +986,35 @@
"widget": "NSpinBox", "widget": "NSpinBox",
"tab": 13, "tab": 13,
"tabLabel": "common.idle", "tabLabel": "common.idle",
"subTab": null "subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
},
{
"labelKey": "panels.idle.custom-label",
"descriptionKey": "panels.idle.custom-description",
"widget": "NLabel",
"tab": 13,
"tabLabel": "common.idle",
"subTab": 1,
"subTabLabel": "panels.idle.tab-custom"
},
{
"labelKey": "panels.idle.custom-entry-timeout",
"descriptionKey": null,
"widget": "NSpinBox",
"tab": 13,
"tabLabel": "common.idle",
"subTab": 1,
"subTabLabel": "panels.idle.tab-custom"
},
{
"labelKey": "panels.idle.custom-entry-command",
"descriptionKey": null,
"widget": "NTextInput",
"tab": 13,
"tabLabel": "common.idle",
"subTab": 1,
"subTabLabel": "panels.idle.tab-custom"
}, },
{ {
"labelKey": "panels.launcher.settings-clipboard-history-label", "labelKey": "panels.launcher.settings-clipboard-history-label",
+4 -3
View File
@@ -722,10 +722,11 @@ Singleton {
// idle management // idle management
property JsonObject idle: JsonObject { property JsonObject idle: JsonObject {
property bool enabled: false property bool enabled: false
property int screenOffTimeout: 0 // minutes, 0 = disabled property int screenOffTimeout: 0 // seconds, 0 = disabled
property int lockTimeout: 0 // minutes, 0 = disabled property int lockTimeout: 0 // seconds, 0 = disabled
property int suspendTimeout: 0 // minutes, 0 = disabled property int suspendTimeout: 0 // seconds, 0 = disabled
property int fadeDuration: 5 // seconds of fade-to-black before action fires property int fadeDuration: 5 // seconds of fade-to-black before action fires
property string customCommands: "[]" // JSON array of {timeout, command}
} }
// desktop widgets // desktop widgets
@@ -0,0 +1,115 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Power
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Master enable
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.idle.enable-label")
description: I18n.tr("panels.idle.enable-description")
checked: Settings.data.idle.enabled
defaultValue: Settings.getDefaultValue("idle.enabled")
onToggled: checked => Settings.data.idle.enabled = checked
}
// Live idle status
RowLayout {
Layout.fillWidth: true
enabled: Settings.data.idle.enabled
visible: IdleService.nativeIdleMonitorAvailable
NLabel {
label: I18n.tr("panels.idle.status-label")
description: I18n.tr("panels.idle.status-description")
}
Item {
Layout.fillWidth: true
}
NText {
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
text: IdleService.idleSeconds > 0 ? I18n.trp("common.second", IdleService.idleSeconds) : I18n.tr("common.active")
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM
color: IdleService.idleSeconds > 0 ? Color.mPrimary : Color.mOnSurfaceVariant
}
}
NLabel {
visible: !IdleService.nativeIdleMonitorAvailable
description: I18n.tr("panels.idle.unavailable")
}
NDivider {
Layout.fillWidth: true
}
// Timeout spinboxes (disabled when idle is off)
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginL
enabled: Settings.data.idle.enabled
NLabel {
label: I18n.tr("panels.idle.timeouts-label")
description: I18n.tr("panels.idle.timeouts-description")
}
NSpinBox {
label: I18n.tr("panels.idle.screen-off-label")
description: I18n.tr("panels.idle.screen-off-description")
from: 0
to: 86400
suffix: "s"
value: Settings.data.idle.screenOffTimeout
defaultValue: 0
onValueChanged: Settings.data.idle.screenOffTimeout = value
}
NSpinBox {
label: I18n.tr("panels.idle.lock-label")
description: I18n.tr("panels.idle.lock-description")
from: 0
to: 86400
suffix: "s"
value: Settings.data.idle.lockTimeout
defaultValue: 0
onValueChanged: Settings.data.idle.lockTimeout = value
}
NSpinBox {
label: I18n.tr("common.suspend")
description: I18n.tr("panels.idle.suspend-description")
from: 0
to: 86400
suffix: "s"
value: Settings.data.idle.suspendTimeout
defaultValue: 0
onValueChanged: Settings.data.idle.suspendTimeout = value
}
NDivider {
Layout.fillWidth: true
}
NSpinBox {
label: I18n.tr("panels.idle.fade-duration-label")
description: I18n.tr("panels.idle.fade-duration-description")
from: 1
to: 60
suffix: "s"
value: Settings.data.idle.fadeDuration
defaultValue: Settings.getDefaultValue("idle.fadeDuration")
onValueChanged: Settings.data.idle.fadeDuration = value
}
}
}
@@ -0,0 +1,158 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Power
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.idle.enabled
property bool _saving: false
ListModel {
id: entriesModel
}
function _loadToModel() {
if (_saving)
return;
entriesModel.clear();
var entries = [];
try {
entries = JSON.parse(Settings.data.idle.customCommands);
} catch (e) {
Logger.w("CustomSubTab", "Failed to parse customCommands:", e);
}
for (var i = 0; i < entries.length; i++) {
entriesModel.append({
"timeout": parseInt(entries[i].timeout) || 60,
"command": String(entries[i].command || "")
});
}
}
function _saveFromModel() {
_saving = true;
var arr = [];
for (var i = 0; i < entriesModel.count; i++) {
var item = entriesModel.get(i);
arr.push({
"timeout": item.timeout,
"command": item.command
});
}
Settings.data.idle.customCommands = JSON.stringify(arr);
_saving = false;
}
Component.onCompleted: _loadToModel()
Connections {
target: Settings.data.idle
function onCustomCommandsChanged() {
root._loadToModel();
}
}
NLabel {
label: I18n.tr("panels.idle.custom-label")
description: I18n.tr("panels.idle.custom-description")
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS
Layout.bottomMargin: Style.marginS
}
Repeater {
model: entriesModel
delegate: ColumnLayout {
id: entryDelegate
required property int index
required property int timeout
required property string command
spacing: Style.marginM
Layout.fillWidth: true
property bool _initialized: false
Component.onCompleted: {
commandInput.text = entryDelegate.command;
_initialized = false;
timeoutSpinBox.value = entryDelegate.timeout;
_initialized = true;
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NSpinBox {
id: timeoutSpinBox
Layout.fillWidth: true
label: I18n.tr("panels.idle.custom-entry-timeout")
from: 1
to: 86400
suffix: "s"
onValueChanged: {
if (entryDelegate._initialized && !root._saving) {
entriesModel.setProperty(entryDelegate.index, "timeout", value);
root._saveFromModel();
}
}
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("panels.idle.custom-entry-delete")
Layout.alignment: Qt.AlignBottom
onClicked: {
entriesModel.remove(entryDelegate.index, 1);
root._saveFromModel();
}
}
}
NTextInput {
id: commandInput
Layout.fillWidth: true
label: I18n.tr("panels.idle.custom-entry-command")
placeholderText: "notify-send \"Idle\""
fontFamily: Settings.data.ui.fontFixed
onTextChanged: {
if (entryDelegate._initialized && !root._saving) {
entriesModel.setProperty(entryDelegate.index, "command", text);
root._saveFromModel();
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS
Layout.bottomMargin: Style.marginS
visible: entryDelegate.index < entriesModel.count - 1
}
}
}
NButton {
text: I18n.tr("panels.idle.custom-add")
icon: "add"
enabled: Settings.data.idle.enabled
onClicked: {
entriesModel.append({
"timeout": 60,
"command": ""
});
root._saveFromModel();
}
}
}
+22 -89
View File
@@ -2,108 +2,41 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import qs.Commons import qs.Commons
import qs.Services.Power
import qs.Widgets import qs.Widgets
ColumnLayout { ColumnLayout {
id: root id: root
spacing: Style.marginL spacing: 0
// Master enable NTabBar {
NToggle { id: subTabBar
Layout.fillWidth: true Layout.fillWidth: true
label: I18n.tr("panels.idle.enable-label") Layout.bottomMargin: Style.marginM
description: I18n.tr("panels.idle.enable-description") distributeEvenly: true
checked: Settings.data.idle.enabled currentIndex: tabView.currentIndex
defaultValue: Settings.getDefaultValue("idle.enabled")
onToggled: checked => Settings.data.idle.enabled = checked
}
// Live idle status NTabButton {
RowLayout { text: I18n.tr("panels.idle.tab-behavior")
Layout.fillWidth: true tabIndex: 0
visible: IdleService.nativeIdleMonitorAvailable checked: subTabBar.currentIndex === 0
NLabel {
label: I18n.tr("panels.idle.status-label")
description: I18n.tr("panels.idle.status-description")
} }
NTabButton {
Item { text: I18n.tr("panels.idle.tab-custom")
Layout.fillWidth: true tabIndex: 1
} checked: subTabBar.currentIndex === 1
NText {
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
text: IdleService.idleSeconds > 0 ? I18n.trp("common.second", IdleService.idleSeconds) : I18n.tr("common.active")
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM
color: IdleService.idleSeconds > 0 ? Color.mPrimary : Color.mOnSurfaceVariant
} }
} }
NLabel { Item {
visible: !IdleService.nativeIdleMonitorAvailable Layout.fillWidth: true
description: I18n.tr("panels.idle.unavailable") Layout.preferredHeight: Style.marginL
} }
NDivider { NTabView {
Layout.fillWidth: true id: tabView
} currentIndex: subTabBar.currentIndex
// Timeout spinboxes (disabled when idle is off) BehaviorSubTab {}
ColumnLayout { CustomSubTab {}
Layout.fillWidth: true
spacing: Style.marginL
enabled: Settings.data.idle.enabled
NLabel {
label: I18n.tr("panels.idle.timeouts-label")
description: I18n.tr("panels.idle.timeouts-description")
}
NSpinBox {
label: I18n.tr("panels.idle.screen-off-label")
description: I18n.tr("panels.idle.screen-off-description")
from: 0
to: 999
value: Settings.data.idle.screenOffTimeout
defaultValue: 0
onValueChanged: Settings.data.idle.screenOffTimeout = value
}
NSpinBox {
label: I18n.tr("panels.idle.lock-label")
description: I18n.tr("panels.idle.lock-description")
from: 0
to: 999
value: Settings.data.idle.lockTimeout
defaultValue: 0
onValueChanged: Settings.data.idle.lockTimeout = value
}
NSpinBox {
label: I18n.tr("common.suspend")
description: I18n.tr("panels.idle.suspend-description")
from: 0
to: 999
value: Settings.data.idle.suspendTimeout
defaultValue: 0
onValueChanged: Settings.data.idle.suspendTimeout = value
}
NDivider {
Layout.fillWidth: true
}
NSpinBox {
label: I18n.tr("panels.idle.fade-duration-label")
description: I18n.tr("panels.idle.fade-duration-description")
from: 1
to: 60
value: Settings.data.idle.fadeDuration
defaultValue: Settings.getDefaultValue("idle.fadeDuration")
onValueChanged: Settings.data.idle.fadeDuration = value
}
} }
} }
@@ -8,6 +8,7 @@ ColumnLayout {
id: root id: root
spacing: Style.marginL spacing: Style.marginL
Layout.fillWidth: true Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
NToggle { NToggle {
label: I18n.tr("panels.notifications.duration-respect-expire-label") label: I18n.tr("panels.notifications.duration-respect-expire-label")
@@ -9,7 +9,7 @@ import qs.Widgets
ColumnLayout { ColumnLayout {
id: root id: root
spacing: Style.marginL
Layout.fillWidth: true Layout.fillWidth: true
property var addMonitor property var addMonitor
@@ -23,119 +23,124 @@ ColumnLayout {
defaultValue: Settings.getDefaultValue("notifications.enabled") defaultValue: Settings.getDefaultValue("notifications.enabled")
} }
NComboBox { ColumnLayout {
label: I18n.tr("panels.notifications.settings-density-label") spacing: Style.marginL
description: I18n.tr("panels.notifications.settings-density-description") enabled: Settings.data.notifications.enabled
model: [
{
"key": "default",
"name": I18n.tr("options.notification-density.default")
},
{
"key": "compact",
"name": I18n.tr("options.notification-density.compact")
}
]
currentKey: Settings.data.notifications.density || "default"
onSelected: key => Settings.data.notifications.density = key
defaultValue: Settings.getDefaultValue("notifications.density")
}
NToggle { NComboBox {
label: I18n.tr("tooltips.do-not-disturb-enabled") label: I18n.tr("panels.notifications.settings-density-label")
description: I18n.tr("panels.notifications.settings-do-not-disturb-description") description: I18n.tr("panels.notifications.settings-density-description")
checked: NotificationService.doNotDisturb model: [
onToggled: checked => NotificationService.doNotDisturb = checked {
} "key": "default",
"name": I18n.tr("options.notification-density.default")
},
{
"key": "compact",
"name": I18n.tr("options.notification-density.compact")
}
]
currentKey: Settings.data.notifications.density || "default"
onSelected: key => Settings.data.notifications.density = key
defaultValue: Settings.getDefaultValue("notifications.density")
}
NComboBox { NToggle {
label: I18n.tr("common.position") label: I18n.tr("tooltips.do-not-disturb-enabled")
description: I18n.tr("panels.notifications.settings-location-description") description: I18n.tr("panels.notifications.settings-do-not-disturb-description")
model: [ checked: NotificationService.doNotDisturb
{ onToggled: checked => NotificationService.doNotDisturb = checked
"key": "top", }
"name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom-center")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
}
]
currentKey: Settings.data.notifications.location || "top_right"
onSelected: key => Settings.data.notifications.location = key
defaultValue: Settings.getDefaultValue("notifications.location")
}
NToggle { NComboBox {
label: I18n.tr("panels.osd.always-on-top-label") label: I18n.tr("common.position")
description: I18n.tr("panels.notifications.settings-always-on-top-description") description: I18n.tr("panels.notifications.settings-location-description")
checked: Settings.data.notifications.overlayLayer model: [
onToggled: checked => Settings.data.notifications.overlayLayer = checked {
defaultValue: Settings.getDefaultValue("notifications.overlayLayer") "key": "top",
} "name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom-center")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
}
]
currentKey: Settings.data.notifications.location || "top_right"
onSelected: key => Settings.data.notifications.location = key
defaultValue: Settings.getDefaultValue("notifications.location")
}
NValueSlider { NToggle {
Layout.fillWidth: true label: I18n.tr("panels.osd.always-on-top-label")
label: I18n.tr("panels.osd.background-opacity-label") description: I18n.tr("panels.notifications.settings-always-on-top-description")
description: I18n.tr("panels.notifications.settings-background-opacity-description") checked: Settings.data.notifications.overlayLayer
from: 0 onToggled: checked => Settings.data.notifications.overlayLayer = checked
to: 1 defaultValue: Settings.getDefaultValue("notifications.overlayLayer")
stepSize: 0.01 }
showReset: true
value: Settings.data.notifications.backgroundOpacity
onMoved: value => Settings.data.notifications.backgroundOpacity = value
text: Math.round(Settings.data.notifications.backgroundOpacity * 100) + "%"
defaultValue: Settings.getDefaultValue("notifications.backgroundOpacity")
}
NDivider { NValueSlider {
Layout.fillWidth: true
}
NText {
text: I18n.tr("panels.notifications.monitors-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true Layout.fillWidth: true
label: modelData.name || I18n.tr("common.unknown") label: I18n.tr("panels.osd.background-opacity-label")
description: { description: I18n.tr("panels.notifications.settings-background-opacity-description")
const compositorScale = CompositorService.getDisplayScale(modelData.name); from: 0
I18n.tr("system.monitor-description", { to: 1
"model": modelData.model, stepSize: 0.01
"width": modelData.width * compositorScale, showReset: true
"height": modelData.height * compositorScale, value: Settings.data.notifications.backgroundOpacity
"scale": compositorScale onMoved: value => Settings.data.notifications.backgroundOpacity = value
}); text: Math.round(Settings.data.notifications.backgroundOpacity * 100) + "%"
} defaultValue: Settings.getDefaultValue("notifications.backgroundOpacity")
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 }
onToggled: checked => {
if (checked) { NDivider {
Settings.data.notifications.monitors = root.addMonitor(Settings.data.notifications.monitors, modelData.name); Layout.fillWidth: true
} else { }
Settings.data.notifications.monitors = root.removeMonitor(Settings.data.notifications.monitors, modelData.name);
NText {
text: I18n.tr("panels.notifications.monitors-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || I18n.tr("common.unknown")
description: {
const compositorScale = CompositorService.getDisplayScale(modelData.name);
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = root.addMonitor(Settings.data.notifications.monitors, modelData.name);
} else {
Settings.data.notifications.monitors = root.removeMonitor(Settings.data.notifications.monitors, modelData.name);
}
} }
} }
} }
} }
} }
@@ -8,6 +8,7 @@ ColumnLayout {
id: root id: root
spacing: Style.marginL spacing: Style.marginL
Layout.fillWidth: true Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
NToggle { NToggle {
label: I18n.tr("panels.notifications.history-clear-dismiss-label") label: I18n.tr("panels.notifications.history-clear-dismiss-label")
@@ -10,6 +10,7 @@ ColumnLayout {
id: root id: root
spacing: Style.marginL spacing: Style.marginL
Layout.fillWidth: true Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
signal openUnifiedPicker signal openUnifiedPicker
signal openLowPicker signal openLowPicker
+66 -5
View File
@@ -20,8 +20,7 @@ import qs.Services.UI
* IdleMonitor instances are created with Qt.createQmlObject() so the shell * IdleMonitor instances are created with Qt.createQmlObject() so the shell
* does not crash on compositors that lack the protocol. * does not crash on compositors that lack the protocol.
* *
* Timeouts come from Settings.data.idle (in minutes). 0 = disabled. * Timeouts come from Settings.data.idle (in seconds). 0 = disabled.
* NOTE: IdleMonitor.timeout is in seconds.
*/ */
Singleton { Singleton {
id: root id: root
@@ -41,6 +40,7 @@ Singleton {
property var _lockMonitor: null property var _lockMonitor: null
property var _suspendMonitor: null property var _suspendMonitor: null
property var _heartbeatMonitor: null property var _heartbeatMonitor: null
property var _customMonitors: ({})
// Signals for external listeners (plugins, modules) // Signals for external listeners (plugins, modules)
signal screenOffRequested signal screenOffRequested
@@ -130,16 +130,76 @@ Singleton {
function onEnabledChanged() { function onEnabledChanged() {
root._applyTimeouts(); root._applyTimeouts();
} }
function onCustomCommandsChanged() {
root._applyCustomMonitors();
}
} }
function _applyTimeouts() { function _applyTimeouts() {
const idle = Settings.data.idle; const idle = Settings.data.idle;
const globalEnabled = idle.enabled; const globalEnabled = idle.enabled;
_setMonitor("screenOff", globalEnabled ? idle.screenOffTimeout * 60 : 0); _setMonitor("screenOff", globalEnabled ? idle.screenOffTimeout : 0);
_setMonitor("lock", globalEnabled ? idle.lockTimeout * 60 : 0); _setMonitor("lock", globalEnabled ? idle.lockTimeout : 0);
_setMonitor("suspend", globalEnabled ? idle.suspendTimeout * 60 : 0); _setMonitor("suspend", globalEnabled ? idle.suspendTimeout : 0);
_ensureHeartbeat(); _ensureHeartbeat();
_applyCustomMonitors();
}
function _applyCustomMonitors() {
// Destroy all existing custom monitors
for (var key in _customMonitors) {
if (_customMonitors[key]) {
_customMonitors[key].destroy();
}
}
root._customMonitors = {};
const idle = Settings.data.idle;
if (!idle.enabled)
return;
var entries = [];
try {
entries = JSON.parse(idle.customCommands);
} catch (e) {
Logger.w("IdleService", "Failed to parse customCommands:", e);
return;
}
var newMonitors = {};
for (var i = 0; i < entries.length; i++) {
const entry = entries[i];
const timeoutSec = parseInt(entry.timeout);
const cmd = entry.command;
if (!cmd || timeoutSec <= 0)
continue;
try {
const qml = `
import Quickshell.Wayland
IdleMonitor { timeout: ${timeoutSec} }
`;
const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_custom_" + i);
const capturedCmd = cmd;
monitor.isIdleChanged.connect(function () {
if (monitor.isIdle) {
root._executeCustomCommand(capturedCmd);
}
});
newMonitors[i] = monitor;
root._monitorsCreated = true;
Logger.i("IdleService", "Custom monitor " + i + " created, timeout", timeoutSec, "s");
} catch (e) {
Logger.w("IdleService", "Failed to create custom monitor " + i + ":", e);
}
}
root._customMonitors = newMonitors;
}
function _executeCustomCommand(cmd) {
Logger.i("IdleService", "Executing custom command:", cmd);
Quickshell.execDetached(["sh", "-c", cmd]);
} }
function _setMonitor(stage, timeoutSec) { function _setMonitor(stage, timeoutSec) {
@@ -198,6 +258,7 @@ Singleton {
const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_heartbeat"); const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_heartbeat");
monitor.isIdleChanged.connect(function () { monitor.isIdleChanged.connect(function () {
if (monitor.isIdle) { if (monitor.isIdle) {
root.idleSeconds = 1;
idleCounter.start(); idleCounter.start();
} else { } else {
idleCounter.stop(); idleCounter.stop();