Power-management software has an awkward requirement: it should not become part of the power problem.
I built PowerSifu, an MIT-licensed GTK tray application for Ubuntu and Debian laptops. It switches power-profiles-daemon profiles when AC power changes, applies optional profile-specific brightness, runs weekly schedules, and can gracefully stop selected user applications when a profile activates.
The first implementation worked, but its monitoring design was too eager. Version 0.3.5 is the result of reworking it around system events instead of repeated polling.
The first design
The original monitor woke every five seconds. It checked whether the laptop was on AC power, asked for the active power profile, compared the result with the saved policy, and acted if anything had changed.
That approach is easy to understand and usually reliable. It is also a poor fit for a utility whose purpose is to reduce unnecessary laptop activity. Even when absolutely nothing changes, a polling loop still wakes up, performs work, and often launches helper processes.
PowerSifu also used powerprofilesctl repeatedly. Besides the process-launch overhead, this exposed the app to a shutdown crash in the Python-based command on a newer Ubuntu environment. Moving the operation into the long-running process solved both problems at once.
Listening instead of asking
Linux already has services that know when these properties change. UPower exposes the power-source state and power-profiles-daemon exposes the active profile. Both are available over D-Bus.
PowerSifu now creates persistent proxies for those services and subscribes to property-change notifications. A transition from AC to battery causes an immediate callback. A profile change does the same. When the laptop sits untouched, the monitor has nothing to poll.
Keeping the proxies alive is important. Recreating a proxy for every read would still repeat service discovery and connection work. The current design creates the service objects once, reuses them for reads and writes, and de-duplicates notifications before applying policy.
The profile write path moved to the power-profiles-daemon system D-Bus interface as well. There is no repeated command-line process between the application and the service.
Events still need a recovery path
An event-driven design should not assume that every desktop session and service behaves perfectly forever. A service can restart, a signal can be missed during reconnection, or an older environment may not expose an expected notification.
For that reason, PowerSifu retains a 60-second reconciliation check. This is not the primary monitor. It is a slow safety net that compares the real system state with the last observed state and repairs any discrepancy.
The distinction matters: normal behavior is immediate and event-driven, while recovery remains bounded and predictable.
Schedules should sleep too
Weekly schedules originally shared the frequent monitoring loop. They only need minute-level precision, so checking every few seconds was unnecessary.
The scheduler now calculates the next wall-clock minute boundary, sleeps until that boundary, and evaluates enabled entries once. If the user has not configured any schedule, the schedule timer is disabled completely.
This also makes the behavior easier to test. The tests can verify alignment, once-per-minute execution, and the no-schedule sleep state independently from AC and profile events.
Brightness without fighting the desktop
Profile-specific brightness presented a different problem. Writing directly to a backlight device can change the panel while leaving GNOME's Quick Settings slider at its old position. That gives the user two conflicting views of the same setting.
On GNOME, PowerSifu therefore changes the desktop's own global brightness control and keeps the visible slider synchronized. Mutter and brightnessctl remain fallbacks for environments where that route is unavailable. External monitors are left to their own controls because they often require DDC/CI or vendor-specific handling.
Keeping the security boundary small
PowerSifu does not install a custom privileged daemon. Profile and brightness changes use the desktop and existing system services. Installing an official update is the one action that requests operating-system authentication. Before that request, the app verifies the download size and SHA-256 digest along with the Debian package name, version, and architecture.
Application rules are intentionally limited. They accept exact process names, match only processes owned by the current user, and send SIGTERM so the application can exit cleanly. There is no arbitrary command field and no root-process termination.
The app has no account, analytics, or telemetry. The update checker contacts the official GitHub Releases API only when the user selects Check for updates.
What changed in 0.3.5
The practical changes are:
- UPower and profile property events replace the five-second polling loop.
- Persistent D-Bus proxies are reused for profile reads and writes.
- Duplicate notifications are filtered before policy is applied.
- A 60-second reconciliation check covers missed or unavailable signals.
- Schedule checks align to minute boundaries and stop when unused.
- Regression tests cover proxy reuse, event de-duplication, and schedule timing.
I am deliberately not claiming a universal battery-life percentage. That depends heavily on the laptop, kernel, desktop, workload, and existing services. The engineering goal here was narrower and measurable in the design: eliminate repeated subprocesses and unnecessary five-second wakeups while preserving fast reactions and recovery.
PowerSifu 0.3.5 is available as a standalone .deb, with source, tests, architecture notes, and release documentation on GitHub:
I would appreciate reports from different Ubuntu and Debian versions, laptop models, and desktop environments. I am also interested in feedback about packaging for distributions beyond the current .deb target.
Top comments (0)