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",
"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",
"lock-description": "Minutes of inactivity before the lock screen activates.",
"lock-description": "Seconds of inactivity before the lock screen activates.",
"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",
"status-description": "Idle time as reported by the compositor.",
"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-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": {
"default-value": "Default: {value}",
+41 -7
View File
@@ -932,7 +932,8 @@
"widget": "NToggle",
"tab": 13,
"tabLabel": "common.idle",
"subTab": null
"subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
},
{
"labelKey": "panels.idle.status-label",
@@ -940,7 +941,8 @@
"widget": "NLabel",
"tab": 13,
"tabLabel": "common.idle",
"subTab": null
"subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
},
{
"labelKey": "panels.idle.timeouts-label",
@@ -948,7 +950,8 @@
"widget": "NLabel",
"tab": 13,
"tabLabel": "common.idle",
"subTab": null
"subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
},
{
"labelKey": "panels.idle.screen-off-label",
@@ -956,7 +959,8 @@
"widget": "NSpinBox",
"tab": 13,
"tabLabel": "common.idle",
"subTab": null
"subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
},
{
"labelKey": "panels.idle.lock-label",
@@ -964,7 +968,8 @@
"widget": "NSpinBox",
"tab": 13,
"tabLabel": "common.idle",
"subTab": null
"subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
},
{
"labelKey": "common.suspend",
@@ -972,7 +977,8 @@
"widget": "NSpinBox",
"tab": 13,
"tabLabel": "common.idle",
"subTab": null
"subTab": 0,
"subTabLabel": "panels.idle.tab-behavior"
},
{
"labelKey": "panels.idle.fade-duration-label",
@@ -980,7 +986,35 @@
"widget": "NSpinBox",
"tab": 13,
"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",
+4 -3
View File
@@ -722,10 +722,11 @@ Singleton {
// idle management
property JsonObject idle: JsonObject {
property bool enabled: false
property int screenOffTimeout: 0 // minutes, 0 = disabled
property int lockTimeout: 0 // minutes, 0 = disabled
property int suspendTimeout: 0 // minutes, 0 = disabled
property int screenOffTimeout: 0 // seconds, 0 = disabled
property int lockTimeout: 0 // seconds, 0 = disabled
property int suspendTimeout: 0 // seconds, 0 = disabled
property int fadeDuration: 5 // seconds of fade-to-black before action fires
property string customCommands: "[]" // JSON array of {timeout, command}
}
// 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.Layouts
import qs.Commons
import qs.Services.Power
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
spacing: 0
// Master enable
NToggle {
NTabBar {
id: subTabBar
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
}
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
// Live idle status
RowLayout {
Layout.fillWidth: true
visible: IdleService.nativeIdleMonitorAvailable
NLabel {
label: I18n.tr("panels.idle.status-label")
description: I18n.tr("panels.idle.status-description")
NTabButton {
text: I18n.tr("panels.idle.tab-behavior")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
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
NTabButton {
text: I18n.tr("panels.idle.tab-custom")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
NLabel {
visible: !IdleService.nativeIdleMonitorAvailable
description: I18n.tr("panels.idle.unavailable")
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NDivider {
Layout.fillWidth: true
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
// 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: 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
}
BehaviorSubTab {}
CustomSubTab {}
}
}
@@ -8,6 +8,7 @@ ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
NToggle {
label: I18n.tr("panels.notifications.duration-respect-expire-label")
@@ -9,7 +9,7 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addMonitor
@@ -23,119 +23,124 @@ ColumnLayout {
defaultValue: Settings.getDefaultValue("notifications.enabled")
}
NComboBox {
label: I18n.tr("panels.notifications.settings-density-label")
description: I18n.tr("panels.notifications.settings-density-description")
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")
}
ColumnLayout {
spacing: Style.marginL
enabled: Settings.data.notifications.enabled
NToggle {
label: I18n.tr("tooltips.do-not-disturb-enabled")
description: I18n.tr("panels.notifications.settings-do-not-disturb-description")
checked: NotificationService.doNotDisturb
onToggled: checked => NotificationService.doNotDisturb = checked
}
NComboBox {
label: I18n.tr("panels.notifications.settings-density-label")
description: I18n.tr("panels.notifications.settings-density-description")
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")
}
NComboBox {
label: I18n.tr("common.position")
description: I18n.tr("panels.notifications.settings-location-description")
model: [
{
"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 {
label: I18n.tr("tooltips.do-not-disturb-enabled")
description: I18n.tr("panels.notifications.settings-do-not-disturb-description")
checked: NotificationService.doNotDisturb
onToggled: checked => NotificationService.doNotDisturb = checked
}
NToggle {
label: I18n.tr("panels.osd.always-on-top-label")
description: I18n.tr("panels.notifications.settings-always-on-top-description")
checked: Settings.data.notifications.overlayLayer
onToggled: checked => Settings.data.notifications.overlayLayer = checked
defaultValue: Settings.getDefaultValue("notifications.overlayLayer")
}
NComboBox {
label: I18n.tr("common.position")
description: I18n.tr("panels.notifications.settings-location-description")
model: [
{
"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 {
Layout.fillWidth: true
label: I18n.tr("panels.osd.background-opacity-label")
description: I18n.tr("panels.notifications.settings-background-opacity-description")
from: 0
to: 1
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")
}
NToggle {
label: I18n.tr("panels.osd.always-on-top-label")
description: I18n.tr("panels.notifications.settings-always-on-top-description")
checked: Settings.data.notifications.overlayLayer
onToggled: checked => Settings.data.notifications.overlayLayer = checked
defaultValue: Settings.getDefaultValue("notifications.overlayLayer")
}
NDivider {
Layout.fillWidth: true
}
NText {
text: I18n.tr("panels.notifications.monitors-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
NValueSlider {
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);
label: I18n.tr("panels.osd.background-opacity-label")
description: I18n.tr("panels.notifications.settings-background-opacity-description")
from: 0
to: 1
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 {
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
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
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
NToggle {
label: I18n.tr("panels.notifications.history-clear-dismiss-label")
@@ -10,6 +10,7 @@ ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
signal openUnifiedPicker
signal openLowPicker
+66 -5
View File
@@ -20,8 +20,7 @@ import qs.Services.UI
* IdleMonitor instances are created with Qt.createQmlObject() so the shell
* does not crash on compositors that lack the protocol.
*
* Timeouts come from Settings.data.idle (in minutes). 0 = disabled.
* NOTE: IdleMonitor.timeout is in seconds.
* Timeouts come from Settings.data.idle (in seconds). 0 = disabled.
*/
Singleton {
id: root
@@ -41,6 +40,7 @@ Singleton {
property var _lockMonitor: null
property var _suspendMonitor: null
property var _heartbeatMonitor: null
property var _customMonitors: ({})
// Signals for external listeners (plugins, modules)
signal screenOffRequested
@@ -130,16 +130,76 @@ Singleton {
function onEnabledChanged() {
root._applyTimeouts();
}
function onCustomCommandsChanged() {
root._applyCustomMonitors();
}
}
function _applyTimeouts() {
const idle = Settings.data.idle;
const globalEnabled = idle.enabled;
_setMonitor("screenOff", globalEnabled ? idle.screenOffTimeout * 60 : 0);
_setMonitor("lock", globalEnabled ? idle.lockTimeout * 60 : 0);
_setMonitor("suspend", globalEnabled ? idle.suspendTimeout * 60 : 0);
_setMonitor("screenOff", globalEnabled ? idle.screenOffTimeout : 0);
_setMonitor("lock", globalEnabled ? idle.lockTimeout : 0);
_setMonitor("suspend", globalEnabled ? idle.suspendTimeout : 0);
_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) {
@@ -198,6 +258,7 @@ Singleton {
const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_heartbeat");
monitor.isIdleChanged.connect(function () {
if (monitor.isIdle) {
root.idleSeconds = 1;
idleCounter.start();
} else {
idleCounter.stop();