1use std::os::unix::net::UnixStream;
15use std::time::Instant;
16
17use wayland_client::protocol::wl_registry;
18use wayland_client::{delegate_noop, Connection, Dispatch, EventQueue, QueueHandle};
19use wayland_protocols_wlr::output_management::v1::client::{
20 zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1,
21 zwlr_output_configuration_v1::{self, ZwlrOutputConfigurationV1},
22 zwlr_output_head_v1::{self, ZwlrOutputHeadV1},
23 zwlr_output_manager_v1::{self, ZwlrOutputManagerV1},
24 zwlr_output_mode_v1::ZwlrOutputModeV1,
25};
26
27use crate::wayland::wlclient::{bounded_roundtrip, impl_sync_callback, SyncState, IO_TIMEOUT};
28
29pub enum ScaleOutcome {
31 Applied,
32 Unsupported,
34}
35
36#[derive(Default)]
37struct OutState {
38 manager: Option<ZwlrOutputManagerV1>,
39 heads: Vec<(ZwlrOutputHeadV1, Option<String>, bool)>,
42 serial: Option<u32>,
43 applied: Option<bool>,
44 sync_done: bool,
45}
46
47impl SyncState for OutState {
48 fn sync_done_mut(&mut self) -> &mut bool {
49 &mut self.sync_done
50 }
51}
52impl_sync_callback!(OutState);
53
54impl Dispatch<wl_registry::WlRegistry, ()> for OutState {
55 fn event(
56 state: &mut Self,
57 registry: &wl_registry::WlRegistry,
58 event: wl_registry::Event,
59 _: &(),
60 _: &Connection,
61 qh: &QueueHandle<Self>,
62 ) {
63 if let wl_registry::Event::Global { name, interface, version } = event {
64 if interface == "zwlr_output_manager_v1" && state.manager.is_none() {
65 state.manager = Some(registry.bind(name, version.min(4), qh, ()));
66 }
67 }
68 }
69}
70
71impl Dispatch<ZwlrOutputManagerV1, ()> for OutState {
72 fn event(
73 state: &mut Self,
74 _: &ZwlrOutputManagerV1,
75 event: zwlr_output_manager_v1::Event,
76 _: &(),
77 _: &Connection,
78 _: &QueueHandle<Self>,
79 ) {
80 match event {
81 zwlr_output_manager_v1::Event::Head { head } => {
82 state.heads.push((head, None, false))
83 }
84 zwlr_output_manager_v1::Event::Done { serial } => state.serial = Some(serial),
87 _ => {}
88 }
89 }
90
91 wayland_client::event_created_child!(OutState, ZwlrOutputManagerV1, [
92 zwlr_output_manager_v1::EVT_HEAD_OPCODE => (ZwlrOutputHeadV1, ()),
93 ]);
94}
95
96impl Dispatch<ZwlrOutputHeadV1, ()> for OutState {
97 fn event(
98 state: &mut Self,
99 head: &ZwlrOutputHeadV1,
100 event: zwlr_output_head_v1::Event,
101 _: &(),
102 _: &Connection,
103 _: &QueueHandle<Self>,
104 ) {
105 let Some(entry) = state.heads.iter_mut().find(|(h, _, _)| h == head) else {
106 return;
107 };
108 match event {
109 zwlr_output_head_v1::Event::Name { name } => entry.1 = Some(name),
110 zwlr_output_head_v1::Event::Enabled { enabled } => entry.2 = enabled != 0,
111 _ => {}
112 }
113 }
114
115 wayland_client::event_created_child!(OutState, ZwlrOutputHeadV1, [
116 zwlr_output_head_v1::EVT_MODE_OPCODE => (ZwlrOutputModeV1, ()),
117 ]);
118}
119
120impl Dispatch<ZwlrOutputConfigurationV1, ()> for OutState {
121 fn event(
122 state: &mut Self,
123 _: &ZwlrOutputConfigurationV1,
124 event: zwlr_output_configuration_v1::Event,
125 _: &(),
126 _: &Connection,
127 _: &QueueHandle<Self>,
128 ) {
129 match event {
130 zwlr_output_configuration_v1::Event::Succeeded => state.applied = Some(true),
131 zwlr_output_configuration_v1::Event::Failed
132 | zwlr_output_configuration_v1::Event::Cancelled => state.applied = Some(false),
133 _ => {}
134 }
135 }
136}
137
138delegate_noop!(OutState: ignore ZwlrOutputModeV1);
139delegate_noop!(OutState: ZwlrOutputConfigurationHeadV1);
140
141pub fn set_output_scale(
145 socket_path: &str,
146 index: usize,
147 scale: f64,
148) -> Result<ScaleOutcome, String> {
149 if !(0.1..=16.0).contains(&scale) {
150 return Err(format!("scale {scale} out of range"));
151 }
152 configure(socket_path, |heads| {
153 let target = heads
154 .get(index)
155 .cloned()
156 .ok_or_else(|| format!("no enabled screen at index {index}"))?;
157 Ok(vec![(target, Plan { scale: Some(scale), ..Plan::default() })])
158 })
159 .map(|changed| if changed == 0 { ScaleOutcome::Unsupported } else { ScaleOutcome::Applied })
160}
161
162pub fn set_screen_geometry(
170 socket_path: &str,
171 index: usize,
172 size: (i32, i32),
173 scale: f64,
174) -> Result<ScaleOutcome, String> {
175 if !(0.1..=16.0).contains(&scale) {
176 return Err(format!("scale {scale} out of range"));
177 }
178 if size.0 <= 0 || size.1 <= 0 {
179 return Err(format!("size {}x{} out of range", size.0, size.1));
180 }
181 configure(socket_path, move |heads| {
182 let target = heads
183 .get(index)
184 .cloned()
185 .ok_or_else(|| format!("no enabled screen at index {index}"))?;
186 Ok(vec![(target, Plan { mode: Some(size), scale: Some(scale) })])
187 })
188 .map(|changed| if changed == 0 { ScaleOutcome::Unsupported } else { ScaleOutcome::Applied })
189}
190
191pub fn hold_spare_screens(
196 socket_path: &str,
197 keep: usize,
198 size: (i32, i32),
199) -> Result<usize, String> {
200 configure(socket_path, move |heads| {
201 Ok(heads
202 .iter()
203 .skip(keep)
204 .cloned()
205 .map(|h| (h, Plan { mode: Some(size), ..Plan::default() }))
206 .collect())
207 })
208}
209
210fn trailing_number(name: &str) -> u32 {
212 let digits: String = name.chars().rev().take_while(|c| c.is_ascii_digit()).collect();
213 digits.chars().rev().collect::<String>().parse().unwrap_or(0)
214}
215
216#[derive(Clone, Copy, Default)]
218struct Plan {
219 mode: Option<(i32, i32)>,
220 scale: Option<f64>,
221}
222
223fn configure<F>(socket_path: &str, plan: F) -> Result<usize, String>
228where
229 F: FnOnce(&[ZwlrOutputHeadV1]) -> Result<Vec<(ZwlrOutputHeadV1, Plan)>, String>,
230{
231 let stream =
232 UnixStream::connect(socket_path).map_err(|e| format!("connect {socket_path}: {e}"))?;
233 let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
234 let mut queue: EventQueue<OutState> = conn.new_event_queue();
235 let qh = queue.handle();
236 let _registry = conn.display().get_registry(&qh, ());
237 let mut state = OutState::default();
238 bounded_roundtrip(&conn, &mut queue, &mut state)?;
239 let Some(manager) = state.manager.clone() else {
240 return Ok(0);
241 };
242 bounded_roundtrip(&conn, &mut queue, &mut state)?;
244 let serial = state.serial.ok_or("output manager sent no state serial")?;
245 let mut named: Vec<(ZwlrOutputHeadV1, String)> = state
249 .heads
250 .iter()
251 .filter(|(_, _, on)| *on)
252 .map(|(h, name, _)| (h.clone(), name.clone().unwrap_or_default()))
253 .collect();
254 named.sort_by_key(|(_, name)| (trailing_number(name), name.clone()));
255 let enabled: Vec<ZwlrOutputHeadV1> = named.into_iter().map(|(h, _)| h).collect();
256 let wanted = plan(&enabled)?;
257 if wanted.is_empty() {
258 manager.stop();
259 let _ = queue.flush();
260 return Ok(0);
261 }
262
263 let config = manager.create_configuration(serial, &qh, ());
265 for head in &enabled {
266 let cfg_head = config.enable_head(head, &qh, ());
267 if let Some((_, want)) = wanted.iter().find(|(h, _)| h == head) {
268 if let Some((w, h)) = want.mode {
269 cfg_head.set_custom_mode(w, h, 0);
270 }
271 if let Some(scale) = want.scale {
272 cfg_head.set_scale(scale);
273 }
274 }
275 }
276 config.apply();
277 queue.flush().map_err(|e| format!("flush configuration: {e}"))?;
278 state.applied = None;
279 let deadline = Instant::now() + IO_TIMEOUT;
280 while state.applied.is_none() && Instant::now() < deadline {
281 bounded_roundtrip(&conn, &mut queue, &mut state)?;
282 }
283 config.destroy();
284 manager.stop();
285 let _ = queue.flush();
286 match state.applied {
287 Some(true) => Ok(wanted.len()),
288 _ => Err("the compositor refused the configuration".to_string()),
289 }
290}