Power
Power policy is opt-in: unlike the idle policy, nothing here runs with the profile. Lid-close suspend and an 80% charge cap change what the machine does, with real consequences on a box you drive remotely, so enabling the profile does not turn them on:
programs.scoot.desktop.power.enable = true;What each default does for your battery:
| Default | Battery effect |
|---|---|
balanced profile (PPD’s boot state) |
none on Apple silicon: there is no PPD driver there (no platform_profile, no EPP; the cores already run schedutil with the deep idle state), so the daemon owns the bus name for widgets and changes nothing. On Intel/AMD with a driver, the middle ground between the other two. On Apple silicon the charge cap and the lid suspend below are what save battery, not profiles |
lid close → suspend |
stops all draw (s2idle on Apple silicon), at the cost of the session, SSH included, sleeping under you |
docked lid close → lock |
keeps drawing for the external screen, behind the lock |
low battery (2%) → suspend |
RAM stays powered on a dying battery: data safety, not savings |
| charge cap 80% | longevity, not runtime: sitting at 100% on the charger is what wears the cell most |
To stretch a flight on driver hardware: switch to power-saver
(Super+p, below), dim earlier (idle.), and let
the lid suspend as it already does. On Apple silicon there is no
driver for profiles to steer (below), so the charge cap and the lid
suspend here are what save battery. To
hold a charge longer on the shelf, the 80% cap is the whole trick.
Profiles
Section titled “Profiles”power-profiles-daemon runs as a system service, owning
org. on the system bus, which is what
applets and shells read, driver or not. Switch one keypress at a time:
| Press | Does |
|---|---|
Super+p |
cycle power-saver → balanced → performance (scoot-power-profile cycle) |
scoot-power-profile statusscoot-power-profile set power-saverpowerprofilesctl listOn Apple silicon the daemon runs a placeholder driver offering
power-saver + balanced. Both are no-ops there, so set to either
succeeds while changing no CPU behavior, and only performance is
refused (with the daemon’s own error). Super+p (cycle) steps only
between the profiles the daemon lists, so the bind never fails silently
from the keymap; set to an unlisted profile fails with an error for
terminal use. Where no driver honors the daemon at all, drop
it and its bind entirely:
programs.scoot.desktop.power = { enable = true; profiles.enable = false;};TLP and auto-cpufreq stay out: both conflict with the daemon (its
module refuses them at eval), and neither has an Apple-silicon
backend.
Holding a profile across plug events is opt-in. The daemon itself holds whatever is set, treating a profile as your intent rather than power state:
programs.scoot.desktop.power = { enable = true; profileOnAC = "performance"; profileOnBattery = "power-saver";};Either half alone works (null keeps the current profile); the
switch applies on boot too (udev coldplug fires for the present
state), while a manual switch mid-session stays until the next plug
event.
Lid, power key, low battery
Section titled “Lid, power key, low battery”The exact rule, with the profile’s docked twin beside it:
| Event | Default | Meaning |
|---|---|---|
| lid close | suspend |
the laptop sleeps (s2idle on Apple silicon) |
| lid close while docked or multi-output | lock |
the session stays up behind the external screen; never suspends |
| lid close on external power, no dock | suspend |
a charger is not a screen |
| power key | suspend |
short press (long press is the firmware’s) |
| battery at 2% | suspend |
through UPower, which already sees the battery |
programs.scoot.desktop.power = { enable = true; # Stay awake on lid close (needs the idle policy's locker to mean # anything), and power off instead of suspending at 2%: # lidSwitch = "lock"; # lowBattery.action = "PowerOff";};Lock-before-sleep ordering: the idle policy’s before-sleep runs
the locker first. swayidle holds logind’s delay inhibitor and
waits (-w), so systemd-suspend. starts only once the lock
is up. Every sleep path here (lid, power key, low battery) goes
through logind suspend, which that inhibitor covers. Suspend keeps the
user manager (no logout; KillUserProcesses stays false), so agents
and multiplexers survive it, but an SSH session sleeping under you does
not resume by itself. Working remotely through a lid close: hold it off
for the session, or make the rule permanent:
systemd-inhibit --what=handle-lid-switch sleep 1dHibernate is not wired. s2idle is the only sleep state the reference
hardware has, and its swap is zram (no persistent image to hibernate
into), so the UPower default (HybridSleep) would fail there instead
of sleeping; the default is suspend. Hibernate and HybridSleep
stay accepted values for hardware with persistent swap and a deeper
sleep state. No idle timer ever suspends (see
Idle and lock); only the battery percentage trips
here.
Charge limit
Section titled “Charge limit”The battery normally stops at 80%: charge_control_end_threshold
on macsmc-battery (Apple silicon) or the first supply that owns
the node (BAT0 on most laptops, so one config travels). Sitting at
100% on the charger is what wears the cell; 80% is the day-to-day
default, and the rest covers the exceptions:
scoot-charge statusscoot-charge togglescoot-charge full-oncescoot-charge limitA full charge once (toggle, until the next unplug), and an
automatic refill to 100% after thirty minutes on battery (a trip
needs the range), dropping back after a day straight on the
charger:
programs.scoot.desktop.power = { enable = true; # A lower cap on a named battery, an hourly trip, a half-day trip end: # chargeLimit.limit = 70; # chargeLimit.battery = "BAT0"; # chargeLimit.fullAfter = 3600; # chargeLimit.tripEndsAfter = 43200; # No cap at all on this box: # chargeLimit.enable = false;};Root re-syncs on every AC change and every five minutes (a missed event is caught within one interval). Where the hardware has no charge-control node the service logs one line and exits cleanly, so one shared config deploys to machines with and without the node. A limit outside 1–100, or a blank battery name, fails evaluation instead.
The state reaches the bar through its push module. Place the
cell and give it the toggle, and the fill unit (which runs when the
bar starts, so a fresh login does not wait for the next sync) does
the rest:
programs.scootbar.settings = { right = [ "charge" "battery" "clock" ]; push.charge.on-click.exec = [ "scoot-charge" "toggle" ];};The button reads 80, full or trip (text, which renders in the
bar’s default font), with the explanation in its tooltip. Every change
is pushed; nothing polls. Without a look the button inherits the bar’s
own colors.
Every value is an option, applied on rebuild/switch (the services restart; no re-login):
| Option | Type | Default | Meaning |
|---|---|---|---|
desktop. |
bool | false (never with the profile) |
run the daemon, the lid/power-key/low-battery policy and the charge service |
desktop. |
bool | true |
run the profiles daemon behind Super+p (false drops the daemon and its bind — for driverless hardware, where the charge limit and the lid policy are what save battery) |
desktop. / . |
enum or null | null (hold) |
profile to select on that power state ("performance", "balanced", "power-saver"); needs profiles. |
desktop. |
logind action | "suspend" |
lid close ("lock" needs the idle policy’s locker) |
desktop. |
logind action | "lock" |
lid close while docked or multi-output; never suspends |
desktop. |
logind action | "suspend" |
lid close on external power without a dock |
desktop. |
logind action | "suspend" |
power key |
desktop. |
int (0–5) | 2 |
battery percent tripping the action (above 5 UPower silently uses its own triple) |
desktop. |
enum | "Suspend" |
the trip ("PowerOff", "Hibernate", "HybridSleep", "Ignore") |
desktop. |
bool | true with the policy |
cap the charge (inert without the sysfs node) |
desktop. |
int (percent, 1–100) | 80 |
the percent the battery normally stops at |
desktop. |
string or null | null (first node found) |
which battery ("BAT0", "macsmc-battery") |
desktop. |
int (seconds) | 1800 |
on-battery time tripping the refill (0 disables the trip) |
desktop. |
int (seconds) | 86400 |
on-charger time ending the trip (0 keeps it until unplug) |
desktop. |
bool | true |
bind the cycle (false leaves Super+p unbound) |
Troubleshooting, by symptom:
Super+pdoes nothing. The slot renders that bind only withpower.beside the keymap (opt-in, never with the profile), and not withenable power.; any of those leaves the combo unbound. Then check the daemon:profiles. enable = false powerprofilesctlfrom a terminal. Outside the session it still lists profiles, so a failure there names the daemon’s own cause.performanceis refused on Apple silicon. The placeholder driver offerspower-saver+balancedonly, both no-ops there. The daemon still owns the bus name for widgets and changes no CPU behavior. (To drop the daemon and its bind entirely, setprofiles..)enable = false - Closing the lid suspends over SSH. That is the default. Hold it off per session (
systemd-inhibit, above) or setlidSwitch = "lock"(needs the idle policy’s locker) or"ignore". - Closing the docked lid suspends. Something beat both docked
rules to logind: your own logind setting wins over the profile’s
(both children default to
lock). - Low battery never suspends. The trip needs UPower awake and
the percentage reached:
systemctl status upowerplusupower -d(the battery must list with a percentage). - The charge never passes 80%.
scoot-charge statusnames the mode (triprefills on its own;togglefills once). Past 80 with the mode alreadylimit, the sysfs node stopped answering: read it (/sys/class/power_supply/BAT0/charge_control_end_threshold) and the service log (systemctl status scoot-charge-sync). - The bar button is empty. The cell needs placing (the two
lines above) and the fill unit needs running:
systemctl --user status scoot-charge-push.
