From b7daf3f64a2ab8aeda313dc32387b769c2624a18 Mon Sep 17 00:00:00 2001 From: purian23 Date: Sat, 30 May 2026 14:57:01 -0400 Subject: [PATCH] feat(ipc): add powerprofile status & shared profile helpers - Follow-up to PR #2515 --- docs/IPC.md | 63 +++++++++++++++++++ quickshell/DMSShellIPC.qml | 61 +++++++----------- quickshell/Modals/PowerProfileModal.qml | 13 ++-- .../ControlCenter/Details/BatteryDetail.qml | 13 ++-- .../Modules/DankBar/Popouts/BatteryPopout.qml | 13 ++-- .../Modules/DankBar/Widgets/Battery.qml | 12 +--- quickshell/Services/PowerProfileWatcher.qml | 60 ++++++++++++++++++ 7 files changed, 171 insertions(+), 64 deletions(-) diff --git a/docs/IPC.md b/docs/IPC.md index 29babecc..8d7751e0 100644 --- a/docs/IPC.md +++ b/docs/IPC.md @@ -282,6 +282,53 @@ dms ipc call inhibit toggle dms ipc call inhibit enable ``` +## Target: `powerprofile` + +Power profile control via `power-profiles-daemon`. Changes stay in sync with DMS UI and trigger the power profile OSD when enabled. + +Requires `power-profiles-daemon` to be installed and running. Works on all compositors. + +### Functions + +**`open`** +- Show the power profile picker modal +- Returns: Success confirmation or error if daemon unavailable + +**`close`** +- Close the power profile picker modal +- Returns: Success confirmation + +**`toggle`** +- Toggle power profile picker modal visibility +- Returns: Success confirmation or error if daemon unavailable + +**`list`** +- List available profile slugs, one per line +- Returns: `power-saver`, `balanced`, and `performance` when supported + +**`status`** +- Get the currently active profile slug +- Returns: `power-saver`, `balanced`, `performance`, or error if daemon unavailable + +**`set `** +- Set the active power profile +- Parameters: Profile slug or alias — `power-saver` (`powersaver`, `saver`, `0`), `balanced` (`1`), `performance` (`2`) +- Returns: Success confirmation or error if profile unknown, unsupported, or write failed + +**`cycle`** +- Cycle to the next available profile in order: power-saver → balanced → performance → power-saver +- Returns: Success confirmation or error if daemon unavailable or write failed + +### Examples +```bash +dms ipc call powerprofile status +dms ipc call powerprofile list +dms ipc call powerprofile cycle +dms ipc call powerprofile set balanced +dms ipc call powerprofile set performance +dms ipc call powerprofile toggle +``` + ## Target: `wallpaper` Wallpaper management and retrieval with support for per-monitor configurations. @@ -543,6 +590,18 @@ Power menu modal control for system power actions. - `close` - Hide power menu modal - `toggle` - Toggle power menu modal visibility +### Target: `powerprofile` +Power profile picker modal and profile control via `power-profiles-daemon`. + +**Functions:** +- `open` - Show power profile picker modal +- `close` - Hide power profile picker modal +- `toggle` - Toggle power profile picker modal visibility +- `list` - List available profile slugs +- `status` - Get current profile slug +- `set ` - Set profile by slug or alias (`power-saver`, `balanced`, `performance`) +- `cycle` - Cycle to the next available profile + ### Target: `control-center` Control Center popout containing network, bluetooth, audio, power, and other quick settings. @@ -673,6 +732,10 @@ dms ipc call processlist toggle # Show power menu dms ipc call powermenu toggle +# Cycle or set power profile (requires power-profiles-daemon) +dms ipc call powerprofile cycle +dms ipc call powerprofile toggle + # Open notepad dms ipc call notepad toggle diff --git a/quickshell/DMSShellIPC.qml b/quickshell/DMSShellIPC.qml index a9eed0f1..da15628d 100644 --- a/quickshell/DMSShellIPC.qml +++ b/quickshell/DMSShellIPC.qml @@ -1894,7 +1894,7 @@ Item { IpcHandler { function open(): string { - if (typeof PowerProfiles === "undefined") + if (!PowerProfileWatcher.available) return "ERROR: power-profiles-daemon not available"; PopoutService.openPowerProfileModal(); @@ -1907,7 +1907,7 @@ Item { } function toggle(): string { - if (typeof PowerProfiles === "undefined") + if (!PowerProfileWatcher.available) return "ERROR: power-profiles-daemon not available"; PopoutService.togglePowerProfileModal(); @@ -1915,59 +1915,46 @@ Item { } function list(): string { - if (typeof PowerProfiles === "undefined") + if (!PowerProfileWatcher.available) return "ERROR: power-profiles-daemon not available"; - const profiles = ["power-saver", "balanced"]; - if (PowerProfiles.hasPerformanceProfile) - profiles.push("performance"); + return PowerProfileWatcher.availableProfiles.map(profile => PowerProfileWatcher.profileSlug(profile)).join("\n"); + } - return profiles.join("\n"); + function status(): string { + if (!PowerProfileWatcher.available) + return "ERROR: power-profiles-daemon not available"; + + return PowerProfileWatcher.profileSlug(PowerProfiles.profile); } function set(profile: string): string { - if (typeof PowerProfiles === "undefined") + if (!PowerProfileWatcher.available) return "ERROR: power-profiles-daemon not available"; if (!profile) return "ERROR: No profile specified"; - const lower = profile.toLowerCase().trim(); - if (lower === "power-saver" || lower === "powersaver" || lower === "saver" || lower === "0") { - PowerProfiles.profile = PowerProfile.PowerSaver; - return "POWERPROFILE_SET_SUCCESS"; - } else if (lower === "balanced" || lower === "1") { - PowerProfiles.profile = PowerProfile.Balanced; - return "POWERPROFILE_SET_SUCCESS"; - } else if (lower === "performance" || lower === "2") { - if (PowerProfiles.hasPerformanceProfile) { - PowerProfiles.profile = PowerProfile.Performance; - return "POWERPROFILE_SET_SUCCESS"; - } else { - return "ERROR: Performance profile not supported by hardware"; - } - } else { + const parsed = PowerProfileWatcher.parseProfileSlug(profile); + if (parsed === -1) return "ERROR: Unknown power profile. Supported options: power-saver, balanced, performance"; - } + + if (parsed === PowerProfile.Performance && !PowerProfiles.hasPerformanceProfile) + return "ERROR: Performance profile not supported by hardware"; + + if (!PowerProfileWatcher.applyProfile(parsed)) + return "ERROR: Failed to set power profile"; + + return "POWERPROFILE_SET_SUCCESS"; } function cycle(): string { - if (typeof PowerProfiles === "undefined") + if (!PowerProfileWatcher.available) return "ERROR: power-profiles-daemon not available"; - const current = PowerProfiles.profile; - const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced]; - if (PowerProfiles.hasPerformanceProfile) - profiles.push(PowerProfile.Performance); + if (!PowerProfileWatcher.cycleProfile()) + return "ERROR: Failed to set power profile"; - const index = profiles.indexOf(current); - if (index === -1) { - PowerProfiles.profile = PowerProfile.Balanced; - return "POWERPROFILE_CYCLE_SUCCESS"; - } - - const nextIndex = (index + 1) % profiles.length; - PowerProfiles.profile = profiles[nextIndex]; return "POWERPROFILE_CYCLE_SUCCESS"; } diff --git a/quickshell/Modals/PowerProfileModal.qml b/quickshell/Modals/PowerProfileModal.qml index 1272e4a7..fdceec0a 100644 --- a/quickshell/Modals/PowerProfileModal.qml +++ b/quickshell/Modals/PowerProfileModal.qml @@ -12,7 +12,7 @@ DankModal { keepPopoutsOpen: true property int selectedIndex: 0 - property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] + property var profileModel: PowerProfileWatcher.availableProfiles function openCentered() { open(); @@ -100,10 +100,15 @@ DankModal { } function setProfile(profile) { - if (typeof PowerProfiles !== "undefined") { - PowerProfiles.profile = profile; + if (PowerProfileWatcher.applyProfile(profile)) { + hideDialog(); + return; } - hideDialog(); + + if (!PowerProfileWatcher.available) + ToastService.showError(I18n.tr("power-profiles-daemon not available")); + else + ToastService.showError(I18n.tr("Failed to set power profile")); } content: Component { diff --git a/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml b/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml index bccd33c4..5510165b 100644 --- a/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml +++ b/quickshell/Modules/ControlCenter/Details/BatteryDetail.qml @@ -24,14 +24,13 @@ Rectangle { } function setProfile(profile) { - if (typeof PowerProfiles === "undefined") { - ToastService.showError(I18n.tr("power-profiles-daemon not available")); + if (PowerProfileWatcher.applyProfile(profile)) return; - } - PowerProfiles.profile = profile; - if (PowerProfiles.profile !== profile) { + + if (!PowerProfileWatcher.available) + ToastService.showError(I18n.tr("power-profiles-daemon not available")); + else ToastService.showError(I18n.tr("Failed to set power profile")); - } } Column { @@ -193,7 +192,7 @@ Rectangle { } DankButtonGroup { - property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] + property var profileModel: PowerProfileWatcher.availableProfiles property int currentProfileIndex: { if (typeof PowerProfiles === "undefined") return 1; diff --git a/quickshell/Modules/DankBar/Popouts/BatteryPopout.qml b/quickshell/Modules/DankBar/Popouts/BatteryPopout.qml index dd1d593b..0e02c978 100644 --- a/quickshell/Modules/DankBar/Popouts/BatteryPopout.qml +++ b/quickshell/Modules/DankBar/Popouts/BatteryPopout.qml @@ -21,14 +21,13 @@ DankPopout { } function setProfile(profile) { - if (typeof PowerProfiles === "undefined") { - ToastService.showError(I18n.tr("power-profiles-daemon not available")); + if (PowerProfileWatcher.applyProfile(profile)) return; - } - PowerProfiles.profile = profile; - if (PowerProfiles.profile !== profile) { + + if (!PowerProfileWatcher.available) + ToastService.showError(I18n.tr("power-profiles-daemon not available")); + else ToastService.showError(I18n.tr("Failed to set power profile")); - } } popupWidth: 400 @@ -555,7 +554,7 @@ DankPopout { DankButtonGroup { id: profileButtonGroup - property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] + property var profileModel: PowerProfileWatcher.availableProfiles property int currentProfileIndex: { if (typeof PowerProfiles === "undefined") return 1; diff --git a/quickshell/Modules/DankBar/Widgets/Battery.qml b/quickshell/Modules/DankBar/Widgets/Battery.qml index 51968709..1f440b60 100644 --- a/quickshell/Modules/DankBar/Widgets/Battery.qml +++ b/quickshell/Modules/DankBar/Widgets/Battery.qml @@ -140,30 +140,24 @@ BasePill { log.info("Trigger! Delta: " + delta); // This is after the other delta checks so it only shows on valid Y scroll - if (typeof PowerProfiles === "undefined") { + if (!PowerProfileWatcher.available) { ToastService.showError(I18n.tr("power-profiles-daemon not available")); return; } - // Get list of profiles, and current index - const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []); + const profiles = PowerProfileWatcher.availableProfiles; var index = profiles.findIndex(profile => PowerProfiles.profile === profile); - // Step once based on mouse wheel direction if (delta > 0) index += 1; else index -= 1; - // Already at end of list, can't go further if (index < 0 || index >= profiles.length) return; - // Set new profile - PowerProfiles.profile = profiles[index]; - if (PowerProfiles.profile !== profiles[index]) { + if (!PowerProfileWatcher.applyProfile(profiles[index])) ToastService.showError(I18n.tr("Failed to set power profile")); - } } } } diff --git a/quickshell/Services/PowerProfileWatcher.qml b/quickshell/Services/PowerProfileWatcher.qml index bab3517a..9d5efe50 100644 --- a/quickshell/Services/PowerProfileWatcher.qml +++ b/quickshell/Services/PowerProfileWatcher.qml @@ -11,8 +11,68 @@ Singleton { property int currentProfile: -1 property int previousProfile: -1 + readonly property bool available: typeof PowerProfiles !== "undefined" + + readonly property var availableProfiles: { + if (!available) + return [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]; + + return [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []); + } + signal profileChanged(int profile) + function profileSlug(profile: int): string { + switch (profile) { + case PowerProfile.PowerSaver: + return "power-saver"; + case PowerProfile.Balanced: + return "balanced"; + case PowerProfile.Performance: + return "performance"; + default: + return "unknown"; + } + } + + function parseProfileSlug(slug: string): int { + if (!slug) + return -1; + + const lower = slug.toLowerCase().trim(); + if (lower === "power-saver" || lower === "powersaver" || lower === "saver" || lower === "0") + return PowerProfile.PowerSaver; + if (lower === "balanced" || lower === "1") + return PowerProfile.Balanced; + if (lower === "performance" || lower === "2") + return PowerProfile.Performance; + return -1; + } + + function applyProfile(profile: int): bool { + if (!available) + return false; + + if (profile === PowerProfile.Performance && !PowerProfiles.hasPerformanceProfile) + return false; + + if (availableProfiles.indexOf(profile) === -1) + return false; + + PowerProfiles.profile = profile; + return PowerProfiles.profile === profile; + } + + function cycleProfile(): bool { + if (!available) + return false; + + const profiles = availableProfiles; + const index = profiles.indexOf(PowerProfiles.profile); + const nextProfile = index === -1 ? PowerProfile.Balanced : profiles[(index + 1) % profiles.length]; + return applyProfile(nextProfile); + } + Connections { target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null