system settings
git clone https://git.lucas.co/cce-system-interface.git
src/bin/cce-power-apply.rs (11.9K)
1 //! cce-power-apply — the root side of the Power page's modes and their
2 //! adapter-state assignment.
3 //!
4 //! The System Interface's Power page keeps a named set of levers per power
5 //! mode plus a table saying which mode runs plugged in and which on battery
6 //! (`/etc/cce/power.kdl`, see `cce_settings::power_plan`). Something has to
7 //! write those levers into sysfs as root whenever the charger comes or goes,
8 //! with no session and no prompt — that is this binary:
9 //!
10 //! - `apply [ac|battery]` — apply the mode assigned to the current (or the
11 //! named) adapter state. Run by `cce-power-apply.service`, which udev
12 //! starts when the Mains supply appears at boot or flips online/offline
13 //! (`udev/90-cce-power-apply.rules`), and by
14 //! `cce-power-apply-resume.service` after every wake. It reads the source
15 //! again when it finishes and re-applies if it moved. Per-lever failures
16 //! are logged and do not fail the run: a missing NVIDIA driver must not
17 //! hide the CPU profile that did land. The plan's battery charge limit,
18 //! which belongs to no mode, is re-applied by every run.
19 //! - `sleep` — lower PCIe ASPM to `powersupersave` before the machine
20 //! sleeps, when the running mode sets it to anything else. Run by
21 //! `cce-power-apply-sleep.service`; the resume unit's `apply` puts the
22 //! mode's own value back.
23 //! - `apply-mode <mode>` — apply one mode by name, whatever is plugged in.
24 //! - `set <mode> <lever> <value|unset>` — record one lever on a mode and,
25 //! when that mode is the one running, apply it now. Run by the Power page
26 //! under pkexec, the app's standard privileged path.
27 //! - `assign <ac|battery> <mode>` — point an adapter state at a mode and,
28 //! when that state is the live one, apply the mode now.
29 //! - `charge-limit <start|unset> <end|unset>` — record the battery's charge
30 //! window (whole percent) and apply it now, whatever is plugged in.
31 //! - `show` — print the plan and the live adapter state.
32 //!
33 //! Installed to `/usr/bin` by `ccebuild install-system` (the udev rule and
34 //! the unit name that path); `ccebuild install` also drops a copy in
35 //! `~/.local/bin`, which the page falls back to under pkexec before the root
36 //! side is installed.
37
38 use cce_settings::power_plan::{
39 apply_charge_limit, apply_lever, apply_mode, current_source, ChargeLimit, Lever, Mode, PowerPlan, Source,
40 PLAN_PATH,
41 };
42
43 fn usage() -> ! {
44 eprintln!(
45 "usage: cce-power-apply apply [ac|battery]\n \
46 cce-power-apply sleep\n \
47 cce-power-apply apply-mode <mode>\n \
48 cce-power-apply set <mode> <lever> <value|unset>\n \
49 cce-power-apply assign <ac|battery> <mode>\n \
50 cce-power-apply charge-limit <start|unset> <end|unset>\n \
51 cce-power-apply show\n\
52 modes: {}\n\
53 levers: {}",
54 Mode::ALL.iter().map(|m| m.key()).collect::<Vec<_>>().join(" "),
55 Lever::ALL.iter().map(|l| l.key()).collect::<Vec<_>>().join(" ")
56 );
57 std::process::exit(2)
58 }
59
60 fn load_plan() -> Result<PowerPlan, i32> {
61 PowerPlan::load().map_err(|e| {
62 eprintln!("cce-power-apply: {}", e);
63 1
64 })
65 }
66
67 /// Apply one mode and report each lever. `what` names what asked for it —
68 /// the adapter state, or the mode itself — so the journal says why.
69 fn run_mode(plan: &PowerPlan, mode: Mode, what: &str) -> i32 {
70 let results = apply_mode(plan, mode);
71 if results.is_empty() {
72 println!("cce-power-apply: {} runs {}, which sets nothing", what, mode.key());
73 return 0;
74 }
75 for (lever, result) in &results {
76 match result {
77 Ok(()) => println!("{} [{}]: {} = {}", what, mode.key(), lever.key(), plan.get(mode, *lever).unwrap_or("")),
78 Err(e) => eprintln!("cce-power-apply: {} [{}] {}: {}", what, mode.key(), lever.key(), e),
79 }
80 }
81 0
82 }
83
84 /// Apply the plan's charge window and report it. Nothing to say when the
85 /// plan has none: the battery's thresholds are then not this binary's.
86 fn run_charge_limit(plan: &PowerPlan) -> i32 {
87 let limit = plan.charge_limit();
88 if limit.is_unset() {
89 return 0;
90 }
91 match apply_charge_limit(limit) {
92 Ok(()) => {
93 println!("charge limit: {}", limit);
94 0
95 }
96 Err(e) => {
97 eprintln!("cce-power-apply: charge limit ({}): {}", limit, e);
98 1
99 }
100 }
101 }
102
103 /// How many times one `apply` follows the source changing under it before
104 /// it gives up and leaves the next udev event to finish the job. A charger
105 /// with a bad contact can flap for as long as it likes.
106 const MAX_APPLY_PASSES: usize = 4;
107
108 fn cmd_apply(forced: Option<&str>) -> i32 {
109 let plan = match load_plan() {
110 Ok(p) => p,
111 Err(code) => return code,
112 };
113 // Every run, not only when the page sets it: the firmware forgets the
114 // thresholds (a pack that runs flat comes back at 0/100), and this run
115 // is what boot, every plug and unplug, and every wake already start.
116 // Its failure is logged and, like a lever's, does not fail the run.
117 run_charge_limit(&plan);
118 if let Some(s) = forced {
119 let source = Source::parse(s).unwrap_or_else(|| usage());
120 return run_mode(&plan, plan.assigned(source), source.key());
121 }
122 // Read the source again after applying, and go round once more if it
123 // moved. A udev event that lands while this run is still going cannot
124 // start another one: `systemctl start` on a oneshot that is already
125 // activating merges into the running job. Until 2026-10-02 that lost a
126 // plug-in at resume: a run started by an unplug as the lid closed was
127 // frozen with the rest of user space, finished two hours later on
128 // resume, and applied the battery mode it had read before sleeping while
129 // the plug-in's event merged into it. Animations stayed off on AC until
130 // the charger was replugged.
131 let mut source = current_source();
132 let mut pass = 1;
133 loop {
134 let code = run_mode(&plan, plan.assigned(source), source.key());
135 let now = current_source();
136 if now == source {
137 return code;
138 }
139 if pass == MAX_APPLY_PASSES {
140 eprintln!(
141 "cce-power-apply: source still changing after {} passes; applied {}, now {}",
142 pass, source.key(), now.key()
143 );
144 return code;
145 }
146 println!("cce-power-apply: source changed to {} while applying {}; applying again", now.key(), source.key());
147 source = now;
148 pass += 1;
149 }
150 }
151
152 /// The ASPM policy every sleep starts under, whatever mode is running.
153 const SLEEP_ASPM: &str = "powersupersave";
154
155 /// Ready the machine to sleep. This laptop has only s2idle, where the
156 /// package reaches S0ix only if its PCIe links can drop into L1 substates,
157 /// and the AC mode's `aspm "performance"` forbids that. Nothing re-applies
158 /// the plan while the machine sleeps, so a suspend begun on the charger kept
159 /// that mode through an unplug: the battery history before 2026-10-04 shows
160 /// sleeps begun on battery drawing about 1 W, and those begun on AC up to
161 /// 4 W, enough to empty a full battery in a day and a half closed.
162 ///
163 /// Only ASPM moves. The rest of a mode either has no effect in s2idle or,
164 /// like the NVIDIA power limit, costs seconds of nvidia-smi on every lid
165 /// close. And only when the running mode sets it: the resume unit's `apply`
166 /// then restores that mode's value, so a mode that leaves ASPM alone is not
167 /// woken into a policy it never chose.
168 fn cmd_sleep() -> i32 {
169 let plan = match load_plan() {
170 Ok(p) => p,
171 Err(code) => return code,
172 };
173 let source = current_source();
174 let mode = plan.assigned(source);
175 match plan.get(mode, Lever::Aspm) {
176 None => {
177 println!("cce-power-apply: sleep: {} [{}] sets no aspm; left as is", source.key(), mode.key());
178 0
179 }
180 Some(v) if v == SLEEP_ASPM => 0,
181 Some(v) => match apply_lever(Lever::Aspm, SLEEP_ASPM) {
182 Ok(()) => {
183 println!("cce-power-apply: sleep: aspm {} -> {} ({} [{}] restores it on wake)", v, SLEEP_ASPM, source.key(), mode.key());
184 0
185 }
186 Err(e) => {
187 // Never fail the unit: a sleep that drains faster beats one
188 // systemd refuses to start.
189 eprintln!("cce-power-apply: sleep: aspm: {}", e);
190 0
191 }
192 },
193 }
194 }
195
196 fn cmd_apply_mode(rest: &[String]) -> i32 {
197 let [mode] = rest else { usage() };
198 let mode = Mode::parse(mode).unwrap_or_else(|| usage());
199 let plan = match load_plan() {
200 Ok(p) => p,
201 Err(code) => return code,
202 };
203 run_mode(&plan, mode, "mode")
204 }
205
206 fn cmd_set(rest: &[String]) -> i32 {
207 let [mode, lever, value] = rest else { usage() };
208 let mode = Mode::parse(mode).unwrap_or_else(|| usage());
209 let lever = Lever::parse(lever).unwrap_or_else(|| usage());
210 let value: Option<&str> = if value == "unset" { None } else { Some(value.as_str()) };
211 let mut plan = match load_plan() {
212 Ok(p) => p,
213 Err(code) => return code,
214 };
215 if let Err(e) = plan.put(mode, lever, value) {
216 eprintln!("cce-power-apply: {}", e);
217 return 2;
218 }
219 if let Err(e) = plan.save() {
220 eprintln!("cce-power-apply: writing {}: {}", PLAN_PATH, e);
221 return 1;
222 }
223 // Only the mode the machine is running right now touches sysfs; editing
224 // any other mode is a plan change and nothing more.
225 if mode == plan.assigned(current_source()) {
226 if let Some(v) = value {
227 if let Err(e) = apply_lever(lever, v) {
228 eprintln!("cce-power-apply: {} {}: {}", mode.key(), lever.key(), e);
229 return 1;
230 }
231 }
232 }
233 0
234 }
235
236 fn cmd_assign(rest: &[String]) -> i32 {
237 let [source, mode] = rest else { usage() };
238 let source = Source::parse(source).unwrap_or_else(|| usage());
239 let mode = Mode::parse(mode).unwrap_or_else(|| usage());
240 let mut plan = match load_plan() {
241 Ok(p) => p,
242 Err(code) => return code,
243 };
244 plan.assign(source, mode);
245 if let Err(e) = plan.save() {
246 eprintln!("cce-power-apply: writing {}: {}", PLAN_PATH, e);
247 return 1;
248 }
249 // Reassigning the live adapter state hands the machine to another mode
250 // now, not at the next unplug.
251 if source == current_source() {
252 return run_mode(&plan, mode, source.key());
253 }
254 0
255 }
256
257 fn cmd_charge_limit(rest: &[String]) -> i32 {
258 let [start, end] = rest else { usage() };
259 let pct = |s: &str| -> Option<u32> {
260 if s == "unset" { None } else { Some(s.parse().unwrap_or_else(|_| usage())) }
261 };
262 let limit = ChargeLimit { start: pct(start), end: pct(end) };
263 let mut plan = match load_plan() {
264 Ok(p) => p,
265 Err(code) => return code,
266 };
267 if let Err(e) = plan.set_charge_limit(limit) {
268 eprintln!("cce-power-apply: {}", e);
269 return 2;
270 }
271 if let Err(e) = plan.save() {
272 eprintln!("cce-power-apply: writing {}: {}", PLAN_PATH, e);
273 return 1;
274 }
275 run_charge_limit(&plan)
276 }
277
278 fn cmd_show() -> i32 {
279 match load_plan() {
280 Ok(plan) => {
281 print!("{}", plan.to_kdl());
282 let source = current_source();
283 println!("// live: {} running {}", source.key(), plan.assigned(source).key());
284 0
285 }
286 Err(code) => code,
287 }
288 }
289
290 fn main() {
291 let args: Vec<String> = std::env::args().skip(1).collect();
292 let code = match args.first().map(String::as_str) {
293 Some("apply") => cmd_apply(args.get(1).map(String::as_str)),
294 Some("sleep") => cmd_sleep(),
295 Some("apply-mode") => cmd_apply_mode(&args[1..]),
296 Some("set") => cmd_set(&args[1..]),
297 Some("assign") => cmd_assign(&args[1..]),
298 Some("charge-limit") => cmd_charge_limit(&args[1..]),
299 Some("show") => cmd_show(),
300 _ => usage(),
301 };
302 std::process::exit(code);
303 }