This commit is contained in:
2026-05-28 21:45:21 +02:00
parent 653439ac24
commit a356a3d369
610 changed files with 205679 additions and 0 deletions
@@ -0,0 +1,220 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.UI
Singleton {
id: root
property bool isInhibited: false
property string reason: I18n.tr("system.user-requested")
property var activeInhibitors: []
property var timeout: null // in seconds
// True when the native Wayland IdleInhibitor is handling inhibition
// (set by the IdleInhibitor element in MainScreen via the nativeInhibitor property)
property bool nativeInhibitorAvailable: false
function init() {
Logger.i("IdleInhibitor", "Service started");
}
// Add an inhibitor
function addInhibitor(id, reason = "Application request") {
if (activeInhibitors.includes(id)) {
Logger.w("IdleInhibitor", "Inhibitor already active:", id);
return false;
}
activeInhibitors.push(id);
updateInhibition(reason);
Logger.d("IdleInhibitor", "Added inhibitor:", id);
return true;
}
// Remove an inhibitor
function removeInhibitor(id) {
const index = activeInhibitors.indexOf(id);
if (index === -1) {
Logger.w("IdleInhibitor", "Inhibitor not found:", id);
return false;
}
activeInhibitors.splice(index, 1);
updateInhibition();
Logger.d("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 (nativeInhibitorAvailable) {
// Native IdleInhibitor in MainScreen handles it via isInhibited binding
Logger.d("IdleInhibitor", "Native inhibitor active");
} else {
startSubprocessInhibition();
}
isInhibited = true;
Logger.i("IdleInhibitor", "Started inhibition:", reason);
}
// Stop system inhibition
function stopInhibition() {
if (!isInhibited)
return;
if (!nativeInhibitorAvailable && inhibitorProcess.running) {
inhibitorProcess.signal(15); // SIGTERM
}
isInhibited = false;
Logger.i("IdleInhibitor", "Stopped inhibition");
}
// Subprocess fallback using systemd-inhibit
function startSubprocessInhibition() {
inhibitorProcess.command = ["systemd-inhibit", "--what=idle", "--why=" + reason, "--mode=block", "sleep", "infinity"];
inhibitorProcess.running = true;
}
// Process for maintaining the inhibition (subprocess fallback only)
Process {
id: inhibitorProcess
running: false
onExited: function (exitCode, exitStatus) {
if (isInhibited) {
Logger.w("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode);
isInhibited = false;
}
}
onStarted: function () {
Logger.d("IdleInhibitor", "Inhibitor process started successfully");
}
}
Timer {
id: inhibitorTimeout
repeat: true
interval: 1000 // 1 second
onTriggered: function () {
if (timeout == null) {
inhibitorTimeout.stop();
return;
}
timeout -= 1;
if (timeout <= 0) {
removeManualInhibitor();
return;
}
}
}
// Manual toggle for user control
function manualToggle() {
// clear any existing timeout
timeout = null;
if (activeInhibitors.includes("manual")) {
removeManualInhibitor();
return false;
} else {
addManualInhibitor(null);
return true;
}
}
function changeTimeout(delta) {
if (timeout == null && delta < 0) {
// no inhibitor, ignored
return;
}
if (timeout == null && delta > 0) {
// enable manual inhibitor and set timeout
addManualInhibitor(timeout + delta);
return;
}
if (timeout + delta <= 0) {
// disable manual inhibitor
removeManualInhibitor();
return;
}
if (timeout + delta > 0) {
// change timeout
addManualInhibitor(timeout + delta);
return;
}
}
function removeManualInhibitor() {
if (timeout !== null) {
timeout = null;
if (inhibitorTimeout.running) {
inhibitorTimeout.stop();
}
}
if (activeInhibitors.includes("manual")) {
removeInhibitor("manual");
ToastService.showNotice(I18n.tr("tooltips.keep-awake"), I18n.tr("common.disabled"), "keep-awake-off");
Logger.i("IdleInhibitor", "Manual inhibition disabled");
}
}
function addManualInhibitor(timeoutSec) {
if (!activeInhibitors.includes("manual")) {
addInhibitor("manual", "Manually activated by user");
ToastService.showNotice(I18n.tr("tooltips.keep-awake"), I18n.tr("common.enabled"), "keep-awake-on");
}
if (timeoutSec === null && timeout === null) {
Logger.i("IdleInhibitor", "Manual inhibition enabled");
return;
} else if (timeoutSec !== null && timeout === null) {
timeout = timeoutSec;
inhibitorTimeout.start();
Logger.i("IdleInhibitor", "Manual inhibition enabled with timeout:", timeoutSec);
return;
} else if (timeoutSec !== null && timeout !== null) {
timeout = timeoutSec;
Logger.i("IdleInhibitor", "Manual inhibition timeout changed to:", timeoutSec);
return;
} else if (timeoutSec === null && timeout !== null) {
timeout = null;
inhibitorTimeout.stop();
Logger.i("IdleInhibitor", "Manual inhibition timeout cleared");
return;
}
}
// Clean up on shutdown
Component.onDestruction: {
stopInhibition();
}
}
@@ -0,0 +1,397 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
/**
* IdleService — native idle detection via ext-idle-notify-v1 Wayland protocol.
*
* Three configurable stages:
* 1. Screen-off (DPMS) — dims / turns off monitors
* 2. Lock screen — activates the session lock
* 3. Suspend — systemctl suspend
*
* Each stage shows a fade-to-black overlay for a configurable grace period
* before executing the action. Any mouse movement cancels the fade.
*
* 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 seconds). 0 = disabled.
*/
Singleton {
id: root
// True if ext-idle-notify-v1 is supported by the compositor
readonly property bool nativeIdleMonitorAvailable: _monitorsCreated
// Live idle time in seconds (updated by the 1s heartbeat monitor)
property int idleSeconds: 0
// Fade overlay state — "" means no fade in progress
property string fadePending: ""
readonly property int fadeDuration: Settings.data.idle.fadeDuration
property bool _monitorsCreated: false
property var _screenOffMonitor: null
property var _lockMonitor: null
property var _suspendMonitor: null
property var _heartbeatMonitor: null
property var _customMonitors: ({})
property var _queuedStages: []
property bool _screenOffActive: false
// Signals for external listeners (plugins, modules)
signal screenOffRequested
signal lockRequested
signal suspendRequested
// -------------------------------------------------------
function init() {
Logger.i("IdleService", "Service started");
_applyTimeouts();
}
// Grace period timer — fires when fade completes without cancellation
Timer {
id: graceTimer
interval: root.fadeDuration * 1000
repeat: false
onTriggered: {
const action = root.fadePending;
root._executeAction(action);
overlayCleanupTimer.start();
}
}
Timer {
id: overlayCleanupTimer
interval: 500
repeat: false
onTriggered: {
root.fadePending = "";
root._runNextQueuedStage();
}
}
// Counts up idleSeconds while the heartbeat monitor reports idle
Timer {
id: idleCounter
interval: 1000
repeat: true
onTriggered: root.idleSeconds++
}
// -------------------------------------------------------
function cancelFade() {
if (fadePending === "") {
_queuedStages = [];
_restoreMonitors();
return;
}
Logger.i("IdleService", "Fade cancelled for:", fadePending);
fadePending = "";
_queuedStages = [];
graceTimer.stop();
overlayCleanupTimer.stop();
_restoreMonitors();
}
function _restoreMonitors() {
if (!_screenOffActive)
return;
_screenOffActive = false;
Logger.i("IdleService", "Restoring monitors (DPMS on)");
CompositorService.turnOnMonitors();
if (Settings.data.idle.resumeScreenOffCommand) {
Logger.i("IdleService", "Executing screen-off resume command");
Quickshell.execDetached(["sh", "-c", Settings.data.idle.resumeScreenOffCommand]);
}
}
function _queueStage(stage) {
if (!_isValidStage(stage)) {
Logger.w("IdleService", "Ignoring unknown queued stage:", stage);
return;
}
if (stage === fadePending)
return;
if (_queuedStages.indexOf(stage) !== -1)
return;
_queuedStages.push(stage);
Logger.d("IdleService", "Queued idle stage while fade is active:", stage);
}
function _isValidStage(stage) {
return stage === "screenOff" || stage === "lock" || stage === "suspend";
}
function _isStageEnabled(stage) {
const idle = Settings.data.idle;
if (stage === "screenOff")
return idle.screenOffTimeout > 0;
if (stage === "lock")
return idle.lockTimeout > 0;
if (stage === "suspend")
return idle.suspendTimeout > 0;
return false;
}
function _runNextQueuedStage() {
if (fadePending !== "")
return;
if (idleSeconds <= 0) {
_queuedStages = [];
return;
}
while (_queuedStages.length > 0) {
const nextStage = _queuedStages.shift();
if (!_isValidStage(nextStage)) {
Logger.w("IdleService", "Dropping queued unknown stage:", nextStage);
continue;
}
if (!_isStageEnabled(nextStage)) {
Logger.d("IdleService", "Dropping queued disabled stage:", nextStage);
continue;
}
Logger.i("IdleService", "Running queued idle stage:", nextStage);
_onIdle(nextStage);
return;
}
}
function _onIdle(stage) {
if (!_isValidStage(stage)) {
Logger.w("IdleService", "Idle fired with unknown stage:", stage);
return;
}
if (!_isStageEnabled(stage)) {
Logger.d("IdleService", "Ignoring idle stage because it is disabled:", stage);
return;
}
if (fadePending !== "") {
_queueStage(stage);
return;
}
Logger.i("IdleService", "Idle fired:", stage);
fadePending = stage;
graceTimer.restart();
}
function _executeAction(stage) {
Logger.i("IdleService", "Executing action:", stage);
if (stage === "screenOff") {
if (Settings.data.idle.screenOffCommand)
Quickshell.execDetached(["sh", "-c", Settings.data.idle.screenOffCommand]);
CompositorService.turnOffMonitors();
root._screenOffActive = true;
root.screenOffRequested();
} else if (stage === "lock") {
if (Settings.data.idle.lockCommand)
Quickshell.execDetached(["sh", "-c", Settings.data.idle.lockCommand]);
if (PanelService.lockScreen && !PanelService.lockScreen.active) {
PanelService.lockScreen.active = true;
}
root.lockRequested();
} else if (stage === "suspend") {
if (Settings.data.idle.suspendCommand)
Quickshell.execDetached(["sh", "-c", Settings.data.idle.suspendCommand]);
if (Settings.data.general.lockOnSuspend) {
CompositorService.lockAndSuspend();
} else {
CompositorService.suspend();
}
root.suspendRequested();
} else {
Logger.w("IdleService", "Unknown idle stage action:", stage);
}
}
// -------------------------------------------------------
// Re-apply when settings change
Connections {
target: Settings
function onSettingsLoaded() {
root._applyTimeouts();
}
}
Connections {
target: Settings.data.idle
function onScreenOffTimeoutChanged() {
root._applyTimeouts();
}
function onLockTimeoutChanged() {
root._applyTimeouts();
}
function onSuspendTimeoutChanged() {
root._applyTimeouts();
}
function onEnabledChanged() {
root._applyTimeouts();
}
function onCustomCommandsChanged() {
root._applyCustomMonitors();
}
}
function _applyTimeouts() {
const idle = Settings.data.idle;
const globalEnabled = idle.enabled;
_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;
const resumeCmd = entry.resumeCommand || "";
if (!cmd && !resumeCmd || timeoutSec <= 0)
continue;
try {
const qml = `
import Quickshell.Wayland
IdleMonitor { timeout: ${timeoutSec} }
`;
const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_custom_" + i);
const capturedCmd = cmd;
const capturedResumeCmd = resumeCmd;
monitor.isIdleChanged.connect(function () {
if (monitor.isIdle) {
if (capturedCmd)
root._executeCustomCommand(capturedCmd);
} else {
if (capturedResumeCmd)
root._executeCustomCommand(capturedResumeCmd);
}
});
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) {
const propName = "_" + stage + "Monitor";
const existing = root[propName];
if (timeoutSec <= 0) {
if (existing) {
existing.destroy();
root[propName] = null;
Logger.d("IdleService", stage + " monitor disabled");
}
return;
}
if (existing) {
if (existing.timeout === timeoutSec)
return;
// ext-idle-notify-v1 has no update-timeout request — must recreate
existing.destroy();
root[propName] = null;
Logger.d("IdleService", stage + " monitor timeout changed to", timeoutSec, "s, recreating");
}
try {
const qml = `
import Quickshell.Wayland
IdleMonitor { timeout: ${timeoutSec} }
`;
const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_" + stage);
monitor.isIdleChanged.connect(function () {
if (monitor.isIdle)
root._onIdle(stage);
else
root.cancelFade();
});
root[propName] = monitor;
root._monitorsCreated = true;
Logger.i("IdleService", stage + " monitor created, timeout", timeoutSec, "s");
} catch (e) {
Logger.w("IdleService", "IdleMonitor not available (compositor lacks ext-idle-notify-v1):", e);
root._monitorsCreated = false;
}
}
function _ensureHeartbeat() {
if (_heartbeatMonitor)
return;
try {
const qml = `
import Quickshell.Wayland
IdleMonitor { timeout: 1 }
`;
const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_heartbeat");
monitor.isIdleChanged.connect(function () {
if (monitor.isIdle) {
root.idleSeconds = 1;
idleCounter.start();
} else {
idleCounter.stop();
root.idleSeconds = 0;
if (root.fadePending === "lock" && Settings.data.idle.resumeLockCommand) {
Logger.i("IdleService", "Executing lock resume command");
Quickshell.execDetached(["sh", "-c", Settings.data.idle.resumeLockCommand]);
} else if (root.fadePending === "suspend" && Settings.data.idle.resumeSuspendCommand) {
Logger.i("IdleService", "Executing suspend resume command");
Quickshell.execDetached(["sh", "-c", Settings.data.idle.resumeSuspendCommand]);
}
root.cancelFade();
overlayCleanupTimer.stop();
}
});
_heartbeatMonitor = monitor;
root._monitorsCreated = true;
Logger.d("IdleService", "Heartbeat monitor created");
} catch (e) {
Logger.w("IdleService", "Heartbeat monitor failed:", e);
}
}
}
@@ -0,0 +1,132 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Services.UI
Singleton {
id: root
readonly property var powerProfiles: PowerProfiles
readonly property bool available: powerProfiles && powerProfiles.hasPerformanceProfile
property int profile: powerProfiles ? powerProfiles.profile : PowerProfile.Balanced
// Not a power profile but a volatile property to quickly disable shadows, animations, etc..
property bool noctaliaPerformanceMode: false
function getName(p) {
if (!available)
return "Unknown";
const prof = (p !== undefined) ? p : profile;
switch (prof) {
case PowerProfile.Performance:
return "Performance";
case PowerProfile.Balanced:
return "Balanced";
case PowerProfile.PowerSaver:
return "Power saver";
default:
return "Unknown";
}
}
function getIcon(p) {
if (!available)
return "balanced";
const prof = (p !== undefined) ? p : profile;
switch (prof) {
case PowerProfile.Performance:
return "performance";
case PowerProfile.Balanced:
return "balanced";
case PowerProfile.PowerSaver:
return "powersaver";
default:
return "balanced";
}
}
function init() {
Logger.d("PowerProfileService", "Service started");
}
function setProfile(p) {
if (!available)
return;
try {
powerProfiles.profile = p;
} catch (e) {
Logger.e("PowerProfileService", "Failed to set profile:", e);
}
}
function cycleProfile() {
if (!available)
return;
const current = powerProfiles.profile;
if (current === PowerProfile.Performance)
setProfile(PowerProfile.PowerSaver);
else if (current === PowerProfile.Balanced)
setProfile(PowerProfile.Performance);
else if (current === PowerProfile.PowerSaver)
setProfile(PowerProfile.Balanced);
}
function cycleProfileReverse() {
if (!available)
return;
const current = powerProfiles.profile;
if (current === PowerProfile.Performance)
setProfile(PowerProfile.Balanced);
else if (current === PowerProfile.Balanced)
setProfile(PowerProfile.PowerSaver);
else if (current === PowerProfile.PowerSaver)
setProfile(PowerProfile.Performance);
}
function isDefault() {
if (!available)
return true;
return (profile === PowerProfile.Balanced);
}
Connections {
target: powerProfiles
function onProfileChanged() {
root.profile = powerProfiles.profile;
// Only show toast if we have a valid profile name (not "Unknown")
const profileName = root.getName();
if (profileName !== "Unknown") {
ToastService.showNotice(I18n.tr("toast.power-profile.profile-name", {
"profile": profileName
}), I18n.tr("toast.power-profile.changed"), profileName.toLowerCase().replace(" ", ""));
}
}
}
// Noctalia Performance Mode
// - Turning shadow off
// - Turning animation off
// - Do Not Disturb
function toggleNoctaliaPerformance() {
noctaliaPerformanceMode = !noctaliaPerformanceMode;
}
function setNoctaliaPerformance(value) {
noctaliaPerformanceMode = value;
}
onNoctaliaPerformanceModeChanged: {
if (noctaliaPerformanceMode) {
ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.enabled"), "rocket");
} else {
ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.disabled"), "rocket-off");
}
}
}