The Problem
Laptop users need to switch between performance profiles depending on workload — power-saver on battery, balanced for daily use, performance when plugged in and gaming. Without shell integration, users dig through system settings or run powerprofilesctl manually.
The Naive Approach
Run cpupower commands directly:
function setPerformance() {
Qt.process(["cpupower", "frequency-set", "-g", "performance"])
}
function setPowerSave() {
Qt.process(["cpupower", "frequency-set", "-g", "powersave"])
}cpupower is CPU-specific and doesn't handle other power-saving measures (screen brightness, PCIe ASPM, NVMe power states, etc.). The modern approach is power-profiles-daemon.
Think of power profiles as gears in a car. First gear (power-saver) gets you moving efficiently but slowly. Third gear (balanced) handles daily driving. Fifth gear (performance) consumes more fuel but delivers maximum speed. The driver shifts gears based on the road ahead.
The Idea
Use power-profiles-daemon (PPD), which provides a D-Bus interface at org.freedesktop.UPower.PowerProfiles. It exposes available profiles, the active profile, and supports switching between them with proper system-wide coordination.
Let's Build It
// PowerService.qml
pragma Singleton
import Quickshell
QtObject {
id: power
readonly property var profiles: ["power-saver", "balanced", "performance"]
property string activeProfile: "balanced"
property bool onBattery: false
function setProfile(profile) {
if (profiles.indexOf(profile) >= 0) {
Qt.process(["powerprofilesctl", "set", profile])
activeProfile = profile
}
}
function cycleProfile() {
var currentIndex = profiles.indexOf(activeProfile)
var nextIndex = (currentIndex + 1) % profiles.length
setProfile(profiles[nextIndex])
}
function refresh() {
Qt.process(["powerprofilesctl", "get"])
.onStdout.connect(function(data) {
activeProfile = data.trim()
})
Qt.process(["cat", "/sys/class/power_supply/AC0/online"])
.onStdout.connect(function(data) {
onBattery = data.trim() !== "1"
})
}
Timer {
interval: 5000; running: true; repeat: true
onTriggered: power.refresh()
}
}Power profile popup:
// PowerControl.qml
PopupWindow {
width: 250; height: 160
anchor.window: panel
Rectangle {
anchors.fill: parent; radius: 8; color: "#1e1e2e"
Column {
anchors { fill: parent; margins: 16 }; spacing: 12
Text { text: "Power Profile"; font.pixelSize: 14; font.bold: true; color: "#cdd6f4" }
Text { text: PowerService.onBattery ? "On Battery" : "Plugged In"; font.pixelSize: 11; color: PowerService.onBattery ? "#fab387" : "#a6e3a1" }
Repeater {
model: PowerService.profiles
delegate: Rectangle {
required property string modelData
width: parent.width; height: 32; radius: 6
color: modelData === PowerService.activeProfile ? "#89b4fa" : "#313244"
opacity: modelData === PowerService.activeProfile ? 1 : 0.6
Text {
text: modelData.charAt(0).toUpperCase() + modelData.slice(1).replace("-", " ")
anchors.centerIn: parent; color: "white"; font.pixelSize: 12; font.bold: modelData === PowerService.activeProfile
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: PowerService.setProfile(modelData)
}
}
}
}
}
}The power service reads and sets profiles via powerprofilesctl. The popup shows available profiles as selectable buttons with the active one highlighted. AC power status is shown to help users decide which profile to use.
Let's Improve It
Automatically switch profiles based on power source:
property bool autoSwitch: true
onOnBatteryChanged: {
if (autoSwitch) {
if (onBattery) {
setProfile("power-saver")
} else {
setProfile("performance")
}
}
}Assuming power-profiles-daemon is installed. PPD is common on GNOME-centric distros but may not be present on Arch, NixOS, or minimal setups. Check for its availability at startup. Fall back to a warning message if not found.
Only switching CPU governor. A power profile is more than CPU frequency. Modern PPD also adjusts: screen brightness, Wi-Fi power save, Bluetooth idle timeout, disk write caching, and GPU power state. Let PPD coordinate all these — don't try to replicate it.
Not handling AC adapter detection. On some laptops, AC0 might be named differently (AC1, ADP1). Check multiple paths or use udevadm to detect the correct power supply device.
Under the Hood
power-profiles-daemon uses D-Bus at org.freedesktop.UPower.PowerProfiles with methods GetProfiles, GetActiveProfile, SetActiveProfile, and property ActiveProfile. The daemon integrates with systemd-logind for inhibitor handling (certain applications can prevent profile switches). It also coordinates with thermald for thermal management and tuned for system tuning profiles.
Exercises
- Use power-profiles-daemon for coordinated system-wide power management
- Automatically switch profiles on AC plug/unplug for convenience
- Display available profiles with the active one highlighted
- Check for PPD availability before showing controls