1use std::collections::HashMap;
42use std::os::unix::net::UnixStream;
43use std::sync::{Mutex, OnceLock};
44use std::time::Instant;
45
46use crate::wayland::kdeproto::output_device::{
47 kde_output_device_mode_v2::{self, KdeOutputDeviceModeV2},
48 kde_output_device_v2::{self, KdeOutputDeviceV2},
49};
50use crate::wayland::kdeproto::output_management::{
51 kde_output_configuration_v2::{self, KdeOutputConfigurationV2},
52 kde_output_management_v2::KdeOutputManagementV2,
53};
54use wayland_client::protocol::wl_registry;
55use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, delegate_noop};
56use wayland_protocols_plasma::screencast::v1::client::{
57 zkde_screencast_stream_unstable_v1::{self, ZkdeScreencastStreamUnstableV1},
58 zkde_screencast_unstable_v1::ZkdeScreencastUnstableV1,
59};
60
61use crate::wayland::outclient::trailing_number;
62use crate::wayland::wlclient::{IO_TIMEOUT, SyncState, bounded_roundtrip, impl_sync_callback};
63
64const POINTER_HIDDEN: u32 = 1;
67
68struct HeldScreen {
70 conn: Connection,
71 _queue: EventQueue<CastState>,
72 _state: CastState,
73 stream: ZkdeScreencastStreamUnstableV1,
74}
75
76fn held_screens() -> &'static Mutex<HashMap<String, HeldScreen>> {
79 static HELD: OnceLock<Mutex<HashMap<String, HeldScreen>>> = OnceLock::new();
80 HELD.get_or_init(Default::default)
81}
82
83#[derive(Default)]
84struct CastState {
85 manager: Option<ZkdeScreencastUnstableV1>,
86 outcome: Option<Result<(), String>>,
88 sync_done: bool,
89}
90
91impl SyncState for CastState {
92 fn sync_done_mut(&mut self) -> &mut bool {
93 &mut self.sync_done
94 }
95}
96impl_sync_callback!(CastState);
97
98impl Dispatch<wl_registry::WlRegistry, ()> for CastState {
99 fn event(
100 state: &mut Self,
101 registry: &wl_registry::WlRegistry,
102 event: wl_registry::Event,
103 _: &(),
104 _: &Connection,
105 qh: &QueueHandle<Self>,
106 ) {
107 if let wl_registry::Event::Global {
108 name,
109 interface,
110 version,
111 } = event
112 {
113 if interface == "zkde_screencast_unstable_v1" && state.manager.is_none() && version >= 2
115 {
116 state.manager = Some(registry.bind(name, version.min(2), qh, ()));
117 }
118 }
119 }
120}
121delegate_noop!(CastState: ZkdeScreencastUnstableV1);
122
123impl Dispatch<ZkdeScreencastStreamUnstableV1, ()> for CastState {
124 fn event(
125 state: &mut Self,
126 _: &ZkdeScreencastStreamUnstableV1,
127 event: zkde_screencast_stream_unstable_v1::Event,
128 _: &(),
129 _: &Connection,
130 _: &QueueHandle<Self>,
131 ) {
132 match event {
133 zkde_screencast_stream_unstable_v1::Event::Created { .. } => {
134 state.outcome = Some(Ok(()))
135 }
136 zkde_screencast_stream_unstable_v1::Event::Failed { error } => {
137 state.outcome = Some(Err(error))
138 }
139 zkde_screencast_stream_unstable_v1::Event::Closed if state.outcome.is_none() => {
140 state.outcome = Some(Err("the compositor closed the stream".to_string()));
141 }
142 _ => {}
143 }
144 }
145}
146
147enum GrowError {
151 Unreachable(String),
152 Refused(String),
153}
154
155fn connect(socket_path: &str) -> Result<(Connection, EventQueue<CastState>, CastState), String> {
158 let stream =
159 UnixStream::connect(socket_path).map_err(|e| format!("connect {socket_path}: {e}"))?;
160 let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
161 let mut queue: EventQueue<CastState> = conn.new_event_queue();
162 let _registry = conn.display().get_registry(&queue.handle(), ());
163 let mut state = CastState::default();
164 bounded_roundtrip(&conn, &mut queue, &mut state)?;
165 Ok((conn, queue, state))
166}
167
168fn grow(
171 socket_path: &str,
172 name: &str,
173 size: (i32, i32),
174 scale: f64,
175) -> Result<HeldScreen, GrowError> {
176 let (conn, mut queue, mut state) = connect(socket_path).map_err(GrowError::Unreachable)?;
177 let qh = queue.handle();
178 let Some(manager) = state.manager.clone() else {
179 return Err(GrowError::Refused(
180 "the compositor offers no zkde_screencast_unstable_v1".to_string(),
181 ));
182 };
183 let stream = manager.stream_virtual_output(
184 name.to_string(),
185 size.0,
186 size.1,
187 scale,
188 POINTER_HIDDEN,
189 &qh,
190 (),
191 );
192 queue
193 .flush()
194 .map_err(|e| GrowError::Unreachable(format!("flush: {e}")))?;
195 let deadline = Instant::now() + IO_TIMEOUT;
196 while state.outcome.is_none() && Instant::now() < deadline {
197 bounded_roundtrip(&conn, &mut queue, &mut state).map_err(GrowError::Unreachable)?;
198 }
199 if !screen_present(socket_path, name).map_err(GrowError::Unreachable)? {
202 return Err(GrowError::Refused(match state.outcome.take() {
203 Some(Err(e)) => format!("virtual screen '{name}' refused: {e}"),
204 _ => format!("virtual screen '{name}' produced no output device"),
205 }));
206 }
207 Ok(HeldScreen {
208 conn,
209 _queue: queue,
210 _state: state,
211 stream,
212 })
213}
214
215fn screen_present(socket_path: &str, name: &str) -> Result<bool, String> {
217 Ok(list_screens(socket_path)?.iter().any(|(n, ..)| n == name))
218}
219
220pub fn screen_control_offered(socket_path: &str) -> Result<bool, String> {
226 let (_conn, _queue, state) = connect(socket_path)?;
227 Ok(state.manager.is_some())
228}
229
230pub fn add_screen(
238 socket_path: &str,
239 name: &str,
240 size: (i32, i32),
241 scale: f64,
242) -> Result<(), String> {
243 if name.is_empty() {
244 return Err("a screen needs a name".to_string());
245 }
246 if size.0 <= 0 || size.1 <= 0 {
247 return Err(format!("size {}x{} out of range", size.0, size.1));
248 }
249 if !(0.1..=16.0).contains(&scale) {
250 return Err(format!("scale {scale} out of range"));
251 }
252 if held_screens().lock().unwrap().contains_key(name) {
253 return Ok(());
254 }
255 let held = grow(socket_path, name, size, scale).map_err(|e| match e {
256 GrowError::Unreachable(e) | GrowError::Refused(e) => e,
257 })?;
258 held_screens()
259 .lock()
260 .unwrap()
261 .insert(name.to_string(), held);
262 Ok(())
263}
264
265pub fn remove_screen(name: &str) -> bool {
269 let Some(held) = held_screens().lock().unwrap().remove(name) else {
270 return false;
271 };
272 held.stream.close();
273 let _ = held.conn.flush();
274 true
275}
276
277struct Device {
279 dev: KdeOutputDeviceV2,
280 name: Option<String>,
281 enabled: bool,
282 pos: (i32, i32),
283 scale: f64,
284 current: Option<KdeOutputDeviceModeV2>,
285}
286
287#[derive(Default)]
288struct KdeOutState {
289 manager: Option<KdeOutputManagementV2>,
290 devices: Vec<Device>,
291 modes: Vec<(KdeOutputDeviceModeV2, (i32, i32))>,
294 applied: Option<bool>,
295 sync_done: bool,
296}
297
298impl SyncState for KdeOutState {
299 fn sync_done_mut(&mut self) -> &mut bool {
300 &mut self.sync_done
301 }
302}
303impl_sync_callback!(KdeOutState);
304
305impl Dispatch<wl_registry::WlRegistry, ()> for KdeOutState {
306 fn event(
307 state: &mut Self,
308 registry: &wl_registry::WlRegistry,
309 event: wl_registry::Event,
310 _: &(),
311 _: &Connection,
312 qh: &QueueHandle<Self>,
313 ) {
314 if let wl_registry::Event::Global {
315 name,
316 interface,
317 version,
318 } = event
319 {
320 if interface == "kde_output_management_v2" && state.manager.is_none() {
321 state.manager = Some(registry.bind(name, version.min(1), qh, ()));
322 } else if interface == "kde_output_device_v2" {
323 let dev: KdeOutputDeviceV2 = registry.bind(name, version.min(2), qh, ());
325 state.devices.push(Device {
326 dev,
327 name: None,
328 enabled: false,
329 pos: (0, 0),
330 scale: 1.0,
331 current: None,
332 });
333 }
334 }
335 }
336}
337delegate_noop!(KdeOutState: KdeOutputManagementV2);
338
339impl Dispatch<KdeOutputDeviceV2, ()> for KdeOutState {
340 fn event(
341 state: &mut Self,
342 dev: &KdeOutputDeviceV2,
343 event: kde_output_device_v2::Event,
344 _: &(),
345 _: &Connection,
346 _: &QueueHandle<Self>,
347 ) {
348 let Some(entry) = state.devices.iter_mut().find(|d| &d.dev == dev) else {
349 return;
350 };
351 match event {
352 kde_output_device_v2::Event::Name { name } => entry.name = Some(name),
353 kde_output_device_v2::Event::Enabled { enabled } => entry.enabled = enabled != 0,
354 kde_output_device_v2::Event::Geometry { x, y, .. } => entry.pos = (x, y),
355 kde_output_device_v2::Event::Scale { factor } => entry.scale = factor,
356 kde_output_device_v2::Event::CurrentMode { mode } => entry.current = Some(mode),
357 _ => {}
358 }
359 }
360
361 wayland_client::event_created_child!(KdeOutState, KdeOutputDeviceV2, [
362 kde_output_device_v2::EVT_MODE_OPCODE => (KdeOutputDeviceModeV2, ()),
363 ]);
364}
365
366impl Dispatch<KdeOutputDeviceModeV2, ()> for KdeOutState {
367 fn event(
368 state: &mut Self,
369 mode: &KdeOutputDeviceModeV2,
370 event: kde_output_device_mode_v2::Event,
371 _: &(),
372 _: &Connection,
373 _: &QueueHandle<Self>,
374 ) {
375 if let kde_output_device_mode_v2::Event::Size { width, height } = event {
376 let mode = mode.clone();
377 state.modes.retain(|(m, _)| *m != mode);
378 state.modes.push((mode, (width, height)));
379 }
380 }
381}
382
383impl Dispatch<KdeOutputConfigurationV2, ()> for KdeOutState {
384 fn event(
385 state: &mut Self,
386 _: &KdeOutputConfigurationV2,
387 event: kde_output_configuration_v2::Event,
388 _: &(),
389 _: &Connection,
390 _: &QueueHandle<Self>,
391 ) {
392 match event {
393 kde_output_configuration_v2::Event::Applied => state.applied = Some(true),
394 kde_output_configuration_v2::Event::Failed => state.applied = Some(false),
395 _ => {}
396 }
397 }
398}
399
400fn read_devices(
403 socket_path: &str,
404) -> Result<(Connection, EventQueue<KdeOutState>, KdeOutState), String> {
405 let stream =
406 UnixStream::connect(socket_path).map_err(|e| format!("connect {socket_path}: {e}"))?;
407 let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
408 let mut queue: EventQueue<KdeOutState> = conn.new_event_queue();
409 let qh = queue.handle();
410 let _registry = conn.display().get_registry(&qh, ());
411 let mut state = KdeOutState::default();
412 bounded_roundtrip(&conn, &mut queue, &mut state)?;
413 bounded_roundtrip(&conn, &mut queue, &mut state)?;
414 Ok((conn, queue, state))
415}
416
417fn enabled_sorted(state: &KdeOutState) -> Vec<usize> {
420 let mut order: Vec<(usize, String)> = state
421 .devices
422 .iter()
423 .enumerate()
424 .filter(|(_, d)| d.enabled)
425 .map(|(i, d)| (i, d.name.clone().unwrap_or_default()))
426 .collect();
427 order.sort_by_key(|(_, name)| (trailing_number(name), name.clone()));
428 order.into_iter().map(|(i, _)| i).collect()
429}
430
431pub fn list_screens(socket_path: &str) -> Result<Vec<super::AppScreen>, String> {
435 let (_conn, _queue, state) = read_devices(socket_path)?;
436 let screens = enabled_sorted(&state)
437 .into_iter()
438 .map(|i| {
439 let d = &state.devices[i];
440 let (w, h) = d
441 .current
442 .as_ref()
443 .and_then(|mode| state.modes.iter().find(|(m, _)| m == mode))
444 .map(|(_, size)| *size)
445 .unwrap_or((0, 0));
446 (d.name.clone().unwrap_or_default(), d.pos.0, d.pos.1, w, h)
447 })
448 .collect();
449 Ok(screens)
450}
451
452pub fn set_screen_layout(
459 socket_path: &str,
460 rects: Vec<(i32, i32, i32, i32)>,
461) -> Result<usize, String> {
462 if rects.iter().any(|(_, _, w, h)| *w <= 0 || *h <= 0) {
463 return Err("a screen rectangle has a non-positive size".to_string());
464 }
465 let (conn, mut queue, mut state) = read_devices(socket_path)?;
466 let Some(manager) = state.manager.clone() else {
467 return Ok(0);
468 };
469 let qh = queue.handle();
470 let order = enabled_sorted(&state);
471 let n = order.len().min(rects.len());
472 let sizes: Vec<(i32, i32)> = order
473 .iter()
474 .zip(&rects)
475 .map(|(&i, r)| {
476 let scale = state.devices[i].scale;
477 (
478 (r.2 as f64 / scale).round() as i32,
479 (r.3 as f64 / scale).round() as i32,
480 )
481 })
482 .collect();
483 let placed: Vec<(KdeOutputDeviceV2, (i32, i32))> = order
484 .iter()
485 .zip(crate::wayland::outclient::close_gaps(&rects[..n], &sizes))
486 .map(|(&i, p)| (state.devices[i].dev.clone(), (p.0, p.1)))
487 .collect();
488 if placed.is_empty() {
489 return Ok(0);
490 }
491 let config = manager.create_configuration(&qh, ());
492 for (dev, (x, y)) in &placed {
493 config.position(dev, *x, *y);
494 }
495 apply_configuration(&conn, &mut queue, &mut state, config)?;
496 Ok(placed.len())
497}
498
499fn apply_configuration(
502 conn: &Connection,
503 queue: &mut EventQueue<KdeOutState>,
504 state: &mut KdeOutState,
505 config: KdeOutputConfigurationV2,
506) -> Result<(), String> {
507 state.applied = None;
508 config.apply();
509 queue
510 .flush()
511 .map_err(|e| format!("flush configuration: {e}"))?;
512 let deadline = Instant::now() + IO_TIMEOUT;
513 while state.applied.is_none() && Instant::now() < deadline {
514 bounded_roundtrip(conn, queue, state)?;
515 }
516 config.destroy();
517 let _ = queue.flush();
518 match state.applied {
519 Some(true) => Ok(()),
520 Some(false) => Err("the compositor refused the configuration".to_string()),
521 None => Err("timed out waiting for the configuration".to_string()),
522 }
523}