atagen

monolith

public
Star0
Watch0

Commit 2e7a7840d045

tarball

atagen <boss@atagen.co>2026-10-19 10:44 UTC

4 files changed+258-237
power: split into dpms, idle blanking and lid modules

power.rs mixed three state machines that share only the Power struct. The lid wake rearm moves into lid::lid_woken so activity() no longer carries lid logic inline.
Deleted file: crates/monolith/src/power.rs−237 deletions
@@ -1,237 +0,0 @@
1-//! Whether each output is lit. Idle blanking, the ipc `power` command and
2-//! wlr-output-power-management all end up here, and the backend is told what
3-//! came out.
4-
5-use crate::logind::Action;
6-use crate::state::{State, now_ns};
7-use monolith_config::LidAction;
8-use monolith_protocols::output_power::{OutputPowerHandler, OutputPowerState};
9-use smithay::output::Output;
10-use smithay::reexports::calloop::RegistrationToken;
11-use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
12-use std::collections::HashSet;
13-use std::time::Duration;
14-
15-/// How long input with the lid shut may go on before the close action runs
16-/// again, so waking a closed laptop does not suspend it mid-keystroke.
17-const LID_GRACE: Duration = Duration::from_secs(5);
18-
19-#[derive(Default)]
20-pub struct Power {
21- /// Every output is dark until the next input.
22- idle_blanked: bool,
23- /// Outputs switched off by name, which input does not wake.
24- off: HashSet<String>,
25- lid_closed: bool,
26- /// The lid close action already ran for this closing.
27- lid_acted: bool,
28- /// The internal panel is dark and out of the layout because the lid is
29- /// shut and something else is connected.
30- panel_off: bool,
31- last_activity: u64,
32- timer: Option<RegistrationToken>,
33-}
34-
35-impl Power {
36- pub fn dark(&self, name: &str) -> bool {
37- self.idle_blanked || self.off.contains(name) || (self.panel_off && internal(name))
38- }
39-
40- /// An inhibitor going away counts as activity, so the screen does not
41- /// blank the moment a video stops.
42- pub fn touch(&mut self) {
43- self.last_activity = now_ns();
44- }
45-
46- pub fn forget(&mut self, name: &str) {
47- self.off.remove(name);
48- }
49-}
50-
51-pub fn internal(name: &str) -> bool {
52- ["eDP-", "LVDS-", "DSI-"]
53- .iter()
54- .any(|p| name.starts_with(p))
55-}
56-
57-impl OutputPowerHandler for State {
58- fn output_power_state(&mut self) -> &mut OutputPowerState {
59- &mut self.protocols.output_power
60- }
61-
62- fn output_power_request(&mut self, output: &Output, on: bool) {
63- let _ = self.set_output_power(Some(&output.name()), on);
64- }
65-
66- fn output_is_powered(&self, output: &Output) -> bool {
67- self.output_powered(&output.name())
68- }
69-}
70-
71-impl State {
72- pub fn output_powered(&self, name: &str) -> bool {
73- !self.services.power.dark(name)
74- }
75-
76- /// Switches outputs on or off by name, every output when `name` is None.
77- /// Turning on also ends an idle blank.
78- pub fn set_output_power(&mut self, name: Option<&str>, on: bool) -> Result<(), String> {
79- let known: Vec<String> = self.wm.space.outputs().map(|o| o.name()).collect();
80- if let Some(n) = name
81- && !known.iter().any(|k| k == n)
82- {
83- return Err(format!("no output named {n}"));
84- }
85- let power = &mut self.services.power;
86- for k in known.into_iter().filter(|k| name.is_none_or(|n| n == k)) {
87- if on {
88- power.off.remove(&k);
89- } else {
90- power.off.insert(k);
91- }
92- }
93- if on {
94- self.services.power.touch();
95- self.wake_idle();
96- }
97- self.apply_power();
98- Ok(())
99- }
100-
101- /// Pushes the wanted power state of every output to the backend.
102- pub fn apply_power(&mut self) {
103- let names: Vec<String> = match self.backend.drm() {
104- Some(drm) => drm.names(),
105- None => self.wm.space.outputs().map(|o| o.name()).collect(),
106- };
107- let power = &mut self.services.power;
108- power.panel_off = power.lid_closed && names.iter().any(|n| !internal(n));
109- let parked = power.panel_off;
110- for name in names {
111- let on = self.output_powered(&name);
112- if let Some(drm) = self.backend.drm_mut() {
113- drm.set_powered(&name, on);
114- }
115- if internal(&name) {
116- crate::drm::set_parked(self, &name, parked);
117- }
118- }
119- let power = &self.services.power;
120- self.protocols.output_power.sync(|o| !power.dark(&o.name()));
121- }
122-
123- /// Notes input, and lights the outputs again if idleness had blanked them.
124- pub fn activity(&mut self) {
125- let seat = self.input.seat.clone();
126- self.services.idle.notifier.notify_activity(&seat);
127- self.services.power.touch();
128- if self.services.power.idle_blanked {
129- self.wake_idle();
130- self.apply_power();
131- }
132- let acts = !matches!(self.settings.config.lid.close, LidAction::Ignore);
133- if acts
134- && self.services.power.lid_closed
135- && std::mem::take(&mut self.services.power.lid_acted)
136- {
137- let again = Timer::from_duration(LID_GRACE);
138- let armed = self.core.handle.insert_source(again, |_, _, state| {
139- state.lid_act();
140- TimeoutAction::Drop
141- });
142- if let Err(e) = armed {
143- tracing::warn!("could not arm the lid action: {e}");
144- }
145- }
146- }
147-
148- /// The lid switch moved. Closing it darkens the internal panel when
149- /// something else shows the desktop, and otherwise runs the configured
150- /// action. Opening it counts as input.
151- pub fn lid_changed(&mut self, closed: bool) {
152- tracing::info!(closed, "lid");
153- self.services.power.lid_closed = closed;
154- self.services.power.lid_acted = false;
155- if !closed {
156- self.activity();
157- }
158- self.apply_power();
159- self.lid_act();
160- }
161-
162- /// Runs the lid close action if the lid is shut with no other output.
163- /// Also called when outputs come and go, since unplugging the last
164- /// external display of a closed laptop is when it should sleep.
165- pub fn lid_act(&mut self) {
166- let lit = self.wm.space.outputs().next().is_some();
167- let power = &mut self.services.power;
168- if !lit || !power.lid_closed || power.lid_acted || power.panel_off {
169- return;
170- }
171- power.lid_acted = true;
172- let action = match self.settings.config.lid.close {
173- LidAction::Suspend => Action::Suspend,
174- LidAction::Poweroff => Action::Poweroff,
175- LidAction::Ignore => return,
176- };
177- crate::logind::act(action);
178- }
179-
180- fn wake_idle(&mut self) {
181- if std::mem::take(&mut self.services.power.idle_blanked) {
182- tracing::info!("input after idle, outputs back on");
183- self.configure_idle();
184- }
185- }
186-
187- /// Arms the idle timer from the config, or disarms it.
188- pub fn configure_idle(&mut self) {
189- if let Some(token) = self.services.power.timer.take() {
190- self.core.handle.remove(token);
191- }
192- let secs = self.settings.config.idle.blank_after;
193- if secs == 0 || self.services.power.idle_blanked || self.backend.drm().is_none() {
194- return;
195- }
196- let power = &mut self.services.power;
197- if power.last_activity == 0 {
198- power.touch();
199- }
200- let timeout = secs.saturating_mul(1_000_000_000);
201- let left = timeout.saturating_sub(now_ns().saturating_sub(power.last_activity));
202- let after = Timer::from_duration(Duration::from_nanos(left.max(1_000_000)));
203- let token = self
204- .core
205- .handle
206- .insert_source(after, |_, _, state| state.idle_elapsed());
207- match token {
208- Ok(token) => self.services.power.timer = Some(token),
209- Err(e) => tracing::warn!("could not arm the idle timer: {e}"),
210- }
211- }
212-
213- fn idle_elapsed(&mut self) -> TimeoutAction {
214- let timeout = self
215- .settings
216- .config
217- .idle
218- .blank_after
219- .saturating_mul(1_000_000_000);
220- if timeout == 0 {
221- self.services.power.timer = None;
222- return TimeoutAction::Drop;
223- }
224- let idle_for = now_ns().saturating_sub(self.services.power.last_activity);
225- if idle_for < timeout {
226- return TimeoutAction::ToDuration(Duration::from_nanos(timeout - idle_for));
227- }
228- if self.services.idle.inhibited {
229- return TimeoutAction::ToDuration(Duration::from_nanos(timeout));
230- }
231- tracing::info!("idle, switching outputs off");
232- self.services.power.timer = None;
233- self.services.power.idle_blanked = true;
234- self.apply_power();
235- TimeoutAction::Drop
236- }
237-}
Added file: crates/monolith/src/power/blank.rs+77 additions
@@ -0,0 +1,77 @@
1+//! Idle blanking, and the input that ends it.
2+
3+use crate::state::{State, now_ns};
4+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
5+use std::time::Duration;
6+
7+impl State {
8+ /// Notes input, and lights the outputs again if idleness had blanked them.
9+ pub fn activity(&mut self) {
10+ let seat = self.input.seat.clone();
11+ self.services.idle.notifier.notify_activity(&seat);
12+ self.services.power.touch();
13+ if self.services.power.idle_blanked {
14+ self.wake_idle();
15+ self.apply_power();
16+ }
17+ self.lid_woken();
18+ }
19+
20+ pub(super) fn wake_idle(&mut self) {
21+ if std::mem::take(&mut self.services.power.idle_blanked) {
22+ tracing::info!("input after idle, outputs back on");
23+ self.configure_idle();
24+ }
25+ }
26+
27+ /// Arms the idle timer from the config, or disarms it.
28+ pub fn configure_idle(&mut self) {
29+ if let Some(token) = self.services.power.timer.take() {
30+ self.core.handle.remove(token);
31+ }
32+ let secs = self.settings.config.idle.blank_after;
33+ if secs == 0 || self.services.power.idle_blanked || self.backend.drm().is_none() {
34+ return;
35+ }
36+ let power = &mut self.services.power;
37+ if power.last_activity == 0 {
38+ power.touch();
39+ }
40+ let timeout = secs.saturating_mul(1_000_000_000);
41+ let left = timeout.saturating_sub(now_ns().saturating_sub(power.last_activity));
42+ let after = Timer::from_duration(Duration::from_nanos(left.max(1_000_000)));
43+ let token = self
44+ .core
45+ .handle
46+ .insert_source(after, |_, _, state| state.idle_elapsed());
47+ match token {
48+ Ok(token) => self.services.power.timer = Some(token),
49+ Err(e) => tracing::warn!("could not arm the idle timer: {e}"),
50+ }
51+ }
52+
53+ fn idle_elapsed(&mut self) -> TimeoutAction {
54+ let timeout = self
55+ .settings
56+ .config
57+ .idle
58+ .blank_after
59+ .saturating_mul(1_000_000_000);
60+ if timeout == 0 {
61+ self.services.power.timer = None;
62+ return TimeoutAction::Drop;
63+ }
64+ let idle_for = now_ns().saturating_sub(self.services.power.last_activity);
65+ if idle_for < timeout {
66+ return TimeoutAction::ToDuration(Duration::from_nanos(timeout - idle_for));
67+ }
68+ if self.services.idle.inhibited {
69+ return TimeoutAction::ToDuration(Duration::from_nanos(timeout));
70+ }
71+ tracing::info!("idle, switching outputs off");
72+ self.services.power.timer = None;
73+ self.services.power.idle_blanked = true;
74+ self.apply_power();
75+ TimeoutAction::Drop
76+ }
77+}
Added file: crates/monolith/src/power/lid.rs+64 additions
@@ -0,0 +1,64 @@
1+//! The lid switch, and what closing it does.
2+
3+use crate::logind::Action;
4+use crate::state::State;
5+use monolith_config::LidAction;
6+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
7+use std::time::Duration;
8+
9+/// How long input with the lid shut may go on before the close action runs
10+/// again, so waking a closed laptop does not suspend it mid-keystroke.
11+const LID_GRACE: Duration = Duration::from_secs(5);
12+
13+impl State {
14+ /// The lid switch moved. Closing it darkens the internal panel when
15+ /// something else shows the desktop, and otherwise runs the configured
16+ /// action. Opening it counts as input.
17+ pub fn lid_changed(&mut self, closed: bool) {
18+ tracing::info!(closed, "lid");
19+ self.services.power.lid_closed = closed;
20+ self.services.power.lid_acted = false;
21+ if !closed {
22+ self.activity();
23+ }
24+ self.apply_power();
25+ self.lid_act();
26+ }
27+
28+ /// Runs the lid close action if the lid is shut with no other output.
29+ /// Also called when outputs come and go, since unplugging the last
30+ /// external display of a closed laptop is when it should sleep.
31+ pub fn lid_act(&mut self) {
32+ let lit = self.wm.space.outputs().next().is_some();
33+ let power = &mut self.services.power;
34+ if !lit || !power.lid_closed || power.lid_acted || power.panel_off {
35+ return;
36+ }
37+ power.lid_acted = true;
38+ let action = match self.settings.config.lid.close {
39+ LidAction::Suspend => Action::Suspend,
40+ LidAction::Poweroff => Action::Poweroff,
41+ LidAction::Ignore => return,
42+ };
43+ crate::logind::act(action);
44+ }
45+
46+ /// Runs the lid close action again a while after input woke a laptop whose
47+ /// lid is still shut, since it only ran once when the lid closed.
48+ pub(super) fn lid_woken(&mut self) {
49+ let acts = !matches!(self.settings.config.lid.close, LidAction::Ignore);
50+ if acts
51+ && self.services.power.lid_closed
52+ && std::mem::take(&mut self.services.power.lid_acted)
53+ {
54+ let again = Timer::from_duration(LID_GRACE);
55+ let armed = self.core.handle.insert_source(again, |_, _, state| {
56+ state.lid_act();
57+ TimeoutAction::Drop
58+ });
59+ if let Err(e) = armed {
60+ tracing::warn!("could not arm the lid action: {e}");
61+ }
62+ }
63+ }
64+}
Added file: crates/monolith/src/power/mod.rs+117 additions
@@ -0,0 +1,117 @@
1+//! Whether each output is lit. Idle blanking, the ipc `power` command and
2+//! wlr-output-power-management all end up here, and the backend is told what
3+//! came out.
4+
5+mod blank;
6+mod lid;
7+
8+use crate::state::{State, now_ns};
9+use monolith_protocols::output_power::{OutputPowerHandler, OutputPowerState};
10+use smithay::output::Output;
11+use smithay::reexports::calloop::RegistrationToken;
12+use std::collections::HashSet;
13+
14+#[derive(Default)]
15+pub struct Power {
16+ /// Every output is dark until the next input.
17+ idle_blanked: bool,
18+ /// Outputs switched off by name, which input does not wake.
19+ off: HashSet<String>,
20+ lid_closed: bool,
21+ /// The lid close action already ran for this closing.
22+ lid_acted: bool,
23+ /// The internal panel is dark and out of the layout because the lid is
24+ /// shut and something else is connected.
25+ panel_off: bool,
26+ last_activity: u64,
27+ timer: Option<RegistrationToken>,
28+}
29+
30+impl Power {
31+ pub fn dark(&self, name: &str) -> bool {
32+ self.idle_blanked || self.off.contains(name) || (self.panel_off && internal(name))
33+ }
34+
35+ /// An inhibitor going away counts as activity, so the screen does not
36+ /// blank the moment a video stops.
37+ pub fn touch(&mut self) {
38+ self.last_activity = now_ns();
39+ }
40+
41+ pub fn forget(&mut self, name: &str) {
42+ self.off.remove(name);
43+ }
44+}
45+
46+pub fn internal(name: &str) -> bool {
47+ ["eDP-", "LVDS-", "DSI-"]
48+ .iter()
49+ .any(|p| name.starts_with(p))
50+}
51+
52+impl OutputPowerHandler for State {
53+ fn output_power_state(&mut self) -> &mut OutputPowerState {
54+ &mut self.protocols.output_power
55+ }
56+
57+ fn output_power_request(&mut self, output: &Output, on: bool) {
58+ let _ = self.set_output_power(Some(&output.name()), on);
59+ }
60+
61+ fn output_is_powered(&self, output: &Output) -> bool {
62+ self.output_powered(&output.name())
63+ }
64+}
65+
66+impl State {
67+ pub fn output_powered(&self, name: &str) -> bool {
68+ !self.services.power.dark(name)
69+ }
70+
71+ /// Switches outputs on or off by name, every output when `name` is None.
72+ /// Turning on also ends an idle blank.
73+ pub fn set_output_power(&mut self, name: Option<&str>, on: bool) -> Result<(), String> {
74+ let known: Vec<String> = self.wm.space.outputs().map(|o| o.name()).collect();
75+ if let Some(n) = name
76+ && !known.iter().any(|k| k == n)
77+ {
78+ return Err(format!("no output named {n}"));
79+ }
80+ let power = &mut self.services.power;
81+ for k in known.into_iter().filter(|k| name.is_none_or(|n| n == k)) {
82+ if on {
83+ power.off.remove(&k);
84+ } else {
85+ power.off.insert(k);
86+ }
87+ }
88+ if on {
89+ self.services.power.touch();
90+ self.wake_idle();
91+ }
92+ self.apply_power();
93+ Ok(())
94+ }
95+
96+ /// Pushes the wanted power state of every output to the backend.
97+ pub fn apply_power(&mut self) {
98+ let names: Vec<String> = match self.backend.drm() {
99+ Some(drm) => drm.names(),
100+ None => self.wm.space.outputs().map(|o| o.name()).collect(),
101+ };
102+ let power = &mut self.services.power;
103+ power.panel_off = power.lid_closed && names.iter().any(|n| !internal(n));
104+ let parked = power.panel_off;
105+ for name in names {
106+ let on = self.output_powered(&name);
107+ if let Some(drm) = self.backend.drm_mut() {
108+ drm.set_powered(&name, on);
109+ }
110+ if internal(&name) {
111+ crate::drm::set_parked(self, &name, parked);
112+ }
113+ }
114+ let power = &self.services.power;
115+ self.protocols.output_power.sync(|o| !power.dark(&o.name()));
116+ }
117+}