atagen

monolith

public
Star0
Watch0

Commit a2cc6a5809c0

tarball

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

5 files changed+231-0
protocols: add wlr-output-power-management-v1

smithay does not ship it in this revision and swayidle-style daemons have no other way to switch monolith's outputs. Requests drive the same `Power` state as the idle timeout, and `sync` reports changes made by anything else back to the client.
Modified file: crates/monolith/src/drm.rs+1 additions
@@ -632,6 +632,7 @@ fn disconnect(state: &mut State, crtc: crtc::Handle) {
632632 state.close_layers(&surface.output);
633633 state.services.lock.forget(&surface.output);
634634 state.services.power.forget(&surface.output.name());
635+ state.protocols.output_power.output_removed(&surface.output);
635636 }
636637
637638 impl Drm {
Modified file: crates/monolith/src/globals.rs+3 additions
@@ -2,6 +2,7 @@
22 //! own handler impl, the rest publish compositor state to clients.
33
44 use crate::state::State;
5+use monolith_protocols::output_power::OutputPowerState;
56 use monolith_protocols::toplevel::ForeignToplevelState;
67 use monolith_protocols::workspace::WorkspaceState;
78 use monolith_render_vk::VulkanRenderer;
@@ -47,6 +48,7 @@ pub struct Protocols {
4748 pub xwayland_shell_state: XWaylandShellState,
4849 pub workspaces: WorkspaceState,
4950 pub foreign_toplevels: ForeignToplevelState,
51+ pub output_power: OutputPowerState,
5052 pub toplevel_list: ForeignToplevelListState,
5153 _image_capture_source: ImageCaptureSourceState,
5254 _idle_inhibit: IdleInhibitManagerState,
@@ -101,6 +103,7 @@ impl Protocols {
101103 xwayland_shell_state: XWaylandShellState::new::<State>(dh),
102104 workspaces: WorkspaceState::new::<State>(dh),
103105 foreign_toplevels: ForeignToplevelState::new::<State>(dh),
106+ output_power: OutputPowerState::new::<State>(dh),
104107 toplevel_list: ForeignToplevelListState::new::<State>(dh),
105108 _image_capture_source: ImageCaptureSourceState::new(),
106109 _idle_inhibit: IdleInhibitManagerState::new::<State>(dh),
Modified file: crates/monolith/src/power.rs+44 additions
@@ -3,6 +3,8 @@
33 //! came out.
44
55 use crate::state::{State, now_ns};
6+use monolith_protocols::output_power::{OutputPowerHandler, OutputPowerState};
7+use smithay::output::Output;
68 use smithay::reexports::calloop::RegistrationToken;
79 use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
810 use std::collections::HashSet;
@@ -34,11 +36,49 @@ impl Power {
3436 }
3537 }
3638
39+impl OutputPowerHandler for State {
40+ fn output_power_state(&mut self) -> &mut OutputPowerState {
41+ &mut self.protocols.output_power
42+ }
43+
44+ fn output_power_request(&mut self, output: &Output, on: bool) {
45+ let _ = self.set_output_power(Some(&output.name()), on);
46+ }
47+
48+ fn output_is_powered(&self, output: &Output) -> bool {
49+ self.output_powered(&output.name())
50+ }
51+}
52+
3753 impl State {
3854 pub fn output_powered(&self, name: &str) -> bool {
3955 !self.services.power.dark(name)
4056 }
4157
58+ /// Switches outputs on or off by name, every output when `name` is None.
59+ /// Turning on also ends an idle blank.
60+ pub fn set_output_power(&mut self, name: Option<&str>, on: bool) -> Result<(), String> {
61+ let known: Vec<String> = self.wm.space.outputs().map(|o| o.name()).collect();
62+ if let Some(n) = name
63+ && !known.iter().any(|k| k == n)
64+ {
65+ return Err(format!("no output named {n}"));
66+ }
67+ let power = &mut self.services.power;
68+ for k in known.into_iter().filter(|k| name.is_none_or(|n| n == k)) {
69+ if on {
70+ power.off.remove(&k);
71+ } else {
72+ power.off.insert(k);
73+ }
74+ }
75+ if on {
76+ self.wake_idle();
77+ }
78+ self.apply_power();
79+ Ok(())
80+ }
81+
4282 /// Pushes the wanted power state of every output to the backend.
4383 pub fn apply_power(&mut self) {
4484 let names: Vec<String> = self.wm.space.outputs().map(|o| o.name()).collect();
@@ -48,6 +88,10 @@ impl State {
4888 drm.set_powered(&name, on);
4989 }
5090 }
91+ let power = &self.services.power;
92+ self.protocols
93+ .output_power
94+ .sync(|o| !power.dark(&o.name()));
5195 }
5296
5397 /// Notes input, and lights the outputs again if idleness had blanked them.
Modified file: crates/protocols/src/lib.rs+1 additions
@@ -1,4 +1,5 @@
11 //! Wayland protocols monolith implements itself.
22
3+pub mod output_power;
34 pub mod toplevel;
45 pub mod workspace;
Added file: crates/protocols/src/output_power.rs+182 additions
@@ -0,0 +1,182 @@
1+//! wlr-output-power-management-v1, so idle daemons like swayidle can switch
2+//! outputs off and on. The compositor owns the power state. This module
3+//! turns requests into handler calls and reports the state back to clients.
4+
5+use smithay::output::Output;
6+use smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::{
7+ zwlr_output_power_manager_v1::{self, ZwlrOutputPowerManagerV1},
8+ zwlr_output_power_v1::{self, ZwlrOutputPowerV1},
9+};
10+use smithay::reexports::wayland_server::backend::{ClientId, GlobalId};
11+use smithay::reexports::wayland_server::{
12+ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
13+};
14+use smithay::wayland::{Dispatch2, GlobalDispatch2};
15+
16+pub trait OutputPowerHandler {
17+ fn output_power_state(&mut self) -> &mut OutputPowerState;
18+ /// A client asked to switch `output` on or off.
19+ fn output_power_request(&mut self, output: &Output, on: bool);
20+ fn output_is_powered(&self, output: &Output) -> bool;
21+}
22+
23+struct Control {
24+ handle: ZwlrOutputPowerV1,
25+ output: Output,
26+ last: bool,
27+}
28+
29+pub struct OutputPowerState {
30+ global: GlobalId,
31+ controls: Vec<Control>,
32+}
33+
34+pub struct ManagerGlobal;
35+
36+pub struct ManagerData;
37+
38+pub struct ControlData;
39+
40+fn mode(on: bool) -> zwlr_output_power_v1::Mode {
41+ if on {
42+ zwlr_output_power_v1::Mode::On
43+ } else {
44+ zwlr_output_power_v1::Mode::Off
45+ }
46+}
47+
48+impl OutputPowerState {
49+ pub fn new<D>(dh: &DisplayHandle) -> Self
50+ where
51+ D: GlobalDispatch<ZwlrOutputPowerManagerV1, ManagerGlobal> + 'static,
52+ {
53+ Self {
54+ global: dh.create_global::<D, ZwlrOutputPowerManagerV1, _>(1, ManagerGlobal),
55+ controls: Vec::new(),
56+ }
57+ }
58+
59+ pub fn global(&self) -> GlobalId {
60+ self.global.clone()
61+ }
62+
63+ /// Tells every client whose view of an output's power is out of date.
64+ pub fn sync(&mut self, powered: impl Fn(&Output) -> bool) {
65+ self.controls.retain(|c| c.handle.is_alive());
66+ for c in &mut self.controls {
67+ let on = powered(&c.output);
68+ if on != c.last {
69+ c.last = on;
70+ c.handle.mode(mode(on));
71+ }
72+ }
73+ }
74+
75+ /// The output is gone, so controls on it are no longer valid.
76+ pub fn output_removed(&mut self, output: &Output) {
77+ self.controls.retain(|c| {
78+ let gone = &c.output == output;
79+ if gone {
80+ c.handle.failed();
81+ }
82+ !gone
83+ });
84+ }
85+}
86+
87+impl<D> GlobalDispatch2<ZwlrOutputPowerManagerV1, D> for ManagerGlobal
88+where
89+ D: OutputPowerHandler + Dispatch<ZwlrOutputPowerManagerV1, ManagerData> + 'static,
90+{
91+ fn bind(
92+ &self,
93+ _state: &mut D,
94+ _dh: &DisplayHandle,
95+ _client: &Client,
96+ resource: New<ZwlrOutputPowerManagerV1>,
97+ data_init: &mut DataInit<'_, D>,
98+ ) {
99+ data_init.init(resource, ManagerData);
100+ }
101+}
102+
103+impl<D> Dispatch2<ZwlrOutputPowerManagerV1, D> for ManagerData
104+where
105+ D: OutputPowerHandler + Dispatch<ZwlrOutputPowerV1, ControlData> + 'static,
106+{
107+ fn request(
108+ &self,
109+ state: &mut D,
110+ _client: &Client,
111+ _resource: &ZwlrOutputPowerManagerV1,
112+ request: zwlr_output_power_manager_v1::Request,
113+ _dh: &DisplayHandle,
114+ data_init: &mut DataInit<'_, D>,
115+ ) {
116+ let zwlr_output_power_manager_v1::Request::GetOutputPower { id, output } = request else {
117+ return;
118+ };
119+ let handle = data_init.init(id, ControlData);
120+ let Some(output) = Output::from_resource(&output) else {
121+ handle.failed();
122+ return;
123+ };
124+ state.output_power_state().controls.retain(|c| c.handle.is_alive());
125+ if state
126+ .output_power_state()
127+ .controls
128+ .iter()
129+ .any(|c| c.output == output)
130+ {
131+ handle.failed();
132+ return;
133+ }
134+ let on = state.output_is_powered(&output);
135+ handle.mode(mode(on));
136+ state.output_power_state().controls.push(Control {
137+ handle,
138+ output,
139+ last: on,
140+ });
141+ }
142+}
143+
144+impl<D: OutputPowerHandler> Dispatch2<ZwlrOutputPowerV1, D> for ControlData {
145+ fn request(
146+ &self,
147+ state: &mut D,
148+ _client: &Client,
149+ resource: &ZwlrOutputPowerV1,
150+ request: zwlr_output_power_v1::Request,
151+ _dh: &DisplayHandle,
152+ _data_init: &mut DataInit<'_, D>,
153+ ) {
154+ let zwlr_output_power_v1::Request::SetMode { mode } = request else {
155+ return;
156+ };
157+ let on = match mode.into_result() {
158+ Ok(zwlr_output_power_v1::Mode::On) => true,
159+ Ok(zwlr_output_power_v1::Mode::Off) => false,
160+ _ => {
161+ resource.post_error(zwlr_output_power_v1::Error::InvalidMode, "invalid mode");
162+ return;
163+ }
164+ };
165+ let output = state
166+ .output_power_state()
167+ .controls
168+ .iter()
169+ .find(|c| c.handle == *resource)
170+ .map(|c| c.output.clone());
171+ if let Some(output) = output {
172+ state.output_power_request(&output, on);
173+ }
174+ }
175+
176+ fn destroyed(&self, state: &mut D, _client: ClientId, resource: &ZwlrOutputPowerV1) {
177+ state
178+ .output_power_state()
179+ .controls
180+ .retain(|c| c.handle != *resource);
181+ }
182+}