1use std::os::unix::io::{AsFd as _, AsRawFd as _};
22use std::time::Duration;
23
24use wayland_client::protocol::{
25 wl_buffer::WlBuffer,
26 wl_compositor::WlCompositor,
27 wl_keyboard::{self, WlKeyboard},
28 wl_output::{self, WlOutput},
29 wl_pointer::{self, WlPointer},
30 wl_registry,
31 wl_seat::WlSeat,
32 wl_shm,
33 wl_shm_pool::WlShmPool,
34 wl_surface::WlSurface,
35};
36use wayland_client::{Connection, Dispatch, QueueHandle, WEnum, delegate_noop};
37use wayland_protocols::ext::idle_notify::v1::client::{
38 ext_idle_notification_v1::{self, ExtIdleNotificationV1},
39 ext_idle_notifier_v1::ExtIdleNotifierV1,
40};
41use wayland_protocols::ext::session_lock::v1::client::{
42 ext_session_lock_manager_v1::ExtSessionLockManagerV1,
43 ext_session_lock_surface_v1::{self, ExtSessionLockSurfaceV1},
44 ext_session_lock_v1::{self, ExtSessionLockV1},
45};
46use wayland_protocols::wp::idle_inhibit::zv1::client::{
47 zwp_idle_inhibit_manager_v1::ZwpIdleInhibitManagerV1, zwp_idle_inhibitor_v1::ZwpIdleInhibitorV1,
48};
49use wayland_protocols::wp::presentation_time::client::{
50 wp_presentation::{self, WpPresentation},
51 wp_presentation_feedback::{self, WpPresentationFeedback},
52};
53use wayland_protocols::xdg::decoration::zv1::client::{
54 zxdg_decoration_manager_v1::ZxdgDecorationManagerV1,
55 zxdg_toplevel_decoration_v1::{self, Mode as DecorationMode, ZxdgToplevelDecorationV1},
56};
57use wayland_protocols::xdg::shell::client::{
58 xdg_popup::XdgPopup,
59 xdg_positioner::{self, XdgPositioner},
60 xdg_surface::{self, XdgSurface},
61 xdg_toplevel::XdgToplevel,
62 xdg_wm_base::{self, XdgWmBase},
63};
64use wayland_protocols_wlr::layer_shell::v1::client::{
65 zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1},
66 zwlr_layer_surface_v1::{self, Anchor, ZwlrLayerSurfaceV1},
67};
68use wayland_protocols_wlr::screencopy::v1::client::{
69 zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
70 zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
71};
72
73const BAR: i32 = 48;
75
76const FRAMES: usize = 5;
78
79#[derive(Default)]
80struct Probe {
81 globals: Vec<(String, u32)>,
83 compositor: Option<WlCompositor>,
84 shm: Option<wl_shm::WlShm>,
85 wm_base: Option<XdgWmBase>,
86 layer_shell: Option<ZwlrLayerShellV1>,
87 screencopy: Option<ZwlrScreencopyManagerV1>,
88 session_lock: Option<ExtSessionLockManagerV1>,
89 idle_notifier: Option<ExtIdleNotifierV1>,
90 idle_inhibit: Option<ZwpIdleInhibitManagerV1>,
91 idle_events: Vec<&'static str>,
95 seat: Option<WlSeat>,
96 layouts: Vec<String>,
100 active_layout: Option<u32>,
101 repeat: Option<(i32, i32)>,
102 pointer_at: Vec<(f64, f64)>,
106 locked: bool,
109 lock_refused: bool,
110 lock_surfaces: Vec<(String, u32, u32)>,
112 decorations: Option<ZxdgDecorationManagerV1>,
113 presentation: Option<WpPresentation>,
114 shot: Option<Shot>,
116 bars_alive: Vec<(WlSurface, ZwlrLayerSurfaceV1)>,
123 outputs: Vec<Screen>,
126 bars: Vec<(String, u32, u32)>,
128 clock: Option<u32>,
130 surface: Option<WlSurface>,
131 configured: bool,
132 presented: Vec<Presented>,
134 discarded: usize,
135}
136
137#[derive(Debug, Default)]
139struct Shot {
140 format: Option<u32>,
142 width: u32,
143 height: u32,
144 stride: u32,
145 described: bool,
147 ready: bool,
149 failed: bool,
150 flags: u32,
152}
153
154#[derive(Debug)]
156struct Screen {
157 output: WlOutput,
158 name: String,
161 width: i32,
162 height: i32,
163 scale: i32,
166}
167
168impl Screen {
169 fn logical(&self) -> (i32, i32) {
172 let scale = self.scale.max(1);
173 (self.width / scale, self.height / scale)
174 }
175}
176
177#[derive(Debug)]
178struct Presented {
179 when: Duration,
181 refresh: Duration,
182 sequence: u64,
183 flags: u32,
184}
185
186impl Probe {
187 fn has(&self, name: &str) -> bool {
188 self.globals.iter().any(|(it, _)| it == name)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::layouts_in_keymap_text;
195
196 const TWO: &str = "\
197xkb_keymap {
198xkb_types \"complete\" {
199\ttype \"ONE_LEVEL\" {
200\t\tlevel_name[1]= \"Any\";
201\t};
202};
203xkb_symbols \"pc_ua_us_2_inet(evdev)\" {
204\tname[1]=\"Ukrainian\";
205\tname[2]=\"English (Dvorak)\";
206};
207};";
208
209 #[test]
212 fn layouts_in_group_order() {
213 assert_eq!(
214 layouts_in_keymap_text(TWO),
215 ["Ukrainian", "English (Dvorak)"]
216 );
217 }
218
219 #[test]
223 fn modifier_levels_are_not_layouts() {
224 assert!(!layouts_in_keymap_text(TWO).iter().any(|it| it == "Any"));
225 }
226
227 #[test]
228 fn a_keymap_that_names_nothing() {
229 assert_eq!(
230 layouts_in_keymap_text("xkb_keymap {};"),
231 Vec::<String>::new()
232 );
233 }
234}
235
236fn main() {
237 let connection = match Connection::connect_to_env() {
238 Ok(connection) => connection,
239 Err(err) => {
240 eprintln!("wl-probe: no compositor to talk to: {err}");
241 eprintln!(" set WAYLAND_DISPLAY to the socket Solium reported.");
242 std::process::exit(2);
243 }
244 };
245 let display = connection.display();
246 let mut queue = connection.new_event_queue();
247 let handle = queue.handle();
248 display.get_registry(&handle, ());
249
250 let mut probe = Probe::default();
251 for _ in 0..2 {
254 if let Err(err) = queue.roundtrip(&mut probe) {
255 eprintln!("wl-probe: the compositor stopped talking: {err}");
256 std::process::exit(2);
257 }
258 }
259
260 if let Ok(hold) = std::env::var("WL_PROBE_WINDOWS") {
270 let seconds: u64 = hold.trim().parse().unwrap_or(20);
271 show_windows(&connection, &mut queue, &mut probe, seconds);
272 return;
273 }
274
275 if let Ok(hold) = std::env::var("WL_PROBE_POPUP") {
279 let seconds: u64 = hold.trim().parse().unwrap_or(20);
280 show_popup(&connection, &mut queue, &mut probe, seconds);
281 return;
282 }
283
284 if let Ok(seconds) = std::env::var("WL_PROBE_KEYBOARD")
287 && let Ok(seconds) = seconds.trim().parse::<u64>()
288 {
289 match watch_keyboard(&connection, &mut queue, &mut probe, seconds) {
290 Ok(()) => return,
291 Err(reason) => {
292 eprintln!("wl-probe: {reason}");
293 std::process::exit(1);
294 }
295 }
296 }
297
298 if std::env::var_os("WL_PROBE_IDLE").is_some() {
302 match watch_idle(&connection, &mut queue, &mut probe) {
303 Ok(()) => return,
304 Err(reason) => {
305 eprintln!("wl-probe: {reason}");
306 std::process::exit(1);
307 }
308 }
309 }
310
311 if let Ok(hold) = std::env::var("WL_PROBE_LOCK") {
315 let seconds: u64 = hold.trim().parse().unwrap_or(10);
316 match lock_the_session(&connection, &mut queue, &mut probe, seconds) {
317 Ok(()) => return,
318 Err(reason) => {
319 eprintln!("wl-probe: {reason}");
320 std::process::exit(1);
321 }
322 }
323 }
324
325 if std::env::var_os("WL_PROBE_CONNECT_ONLY").is_some() {
332 println!("connected, {} globals, leaving", probe.globals.len());
333 return;
334 }
335
336 let mut failures = Vec::new();
337
338 println!("globals: {}", probe.globals.len());
339 for wanted in [
340 "wl_compositor",
341 "wl_shm",
342 "xdg_wm_base",
343 "wp_presentation",
344 "wp_viewporter",
345 "wp_fractional_scale_manager_v1",
346 "xdg_activation_v1",
347 "zwp_relative_pointer_manager_v1",
348 "zwp_pointer_constraints_v1",
349 "zwp_primary_selection_device_manager_v1",
350 "zxdg_decoration_manager_v1",
351 "zwlr_layer_shell_v1",
352 "zwlr_screencopy_manager_v1",
353 "ext_session_lock_manager_v1",
354 "ext_idle_notifier_v1",
355 "zwp_idle_inhibit_manager_v1",
356 ] {
357 let version = probe
358 .globals
359 .iter()
360 .find(|(it, _)| it == wanted)
361 .map(|(_, version)| *version);
362 match version {
363 Some(version) => println!(" ok {wanted} v{version}"),
364 None => {
365 println!(" MISSING {wanted}");
366 failures.push(format!("{wanted} is not advertised"));
367 }
368 }
369 }
370
371 println!();
372 match (probe.layouts.is_empty(), probe.repeat) {
373 (true, _) => {
374 println!("keyboard: the compositor sent no keymap");
375 failures.push("no keymap arrived on wl_keyboard".to_owned());
376 }
377 (false, repeat) => {
378 let active = probe.active_layout.unwrap_or(0) as usize;
379 let named: Vec<String> = probe
380 .layouts
381 .iter()
382 .enumerate()
383 .map(|(index, name)| {
384 if index == active {
385 format!("[{name}]")
386 } else {
387 name.clone()
388 }
389 })
390 .collect();
391 println!("keyboard layouts: {}", named.join(" "));
392 match repeat {
393 Some((rate, delay)) => println!("keyboard repeat: {rate}/s after {delay}ms"),
394 None => println!("keyboard repeat: never told (wl_seat below version 4)"),
395 }
396 }
397 }
398
399 println!();
400 println!("outputs: {}", probe.outputs.len());
401 for screen in &probe.outputs {
402 let name = if screen.name.is_empty() {
403 "(no name — wl_output is below version 4)"
407 } else {
408 &screen.name
409 };
410 let (logical_width, logical_height) = screen.logical();
411 if screen.scale > 1 {
412 println!(
413 " {name} {}x{} at {}x ({logical_width}x{logical_height} logical)",
414 screen.width, screen.height, screen.scale
415 );
416 } else {
417 println!(" {name} {}x{}", screen.width, screen.height);
418 }
419 }
420 if probe.outputs.iter().any(|screen| screen.name.is_empty()) {
421 failures.push("an output arrived with no name".to_owned());
422 }
423
424 if probe.has("zwlr_layer_shell_v1") {
427 match anchor_a_bar(&connection, &mut queue, &mut probe) {
428 Ok(()) => {}
429 Err(reason) => failures.push(reason),
430 }
431 }
432
433 if probe.has("zwlr_screencopy_manager_v1") {
436 for region in [None, Some((200, 300, 400, 250))] {
439 match take_a_shot(&connection, &mut queue, &mut probe, region) {
440 Ok(()) => {}
441 Err(reason) => failures.push(reason),
442 }
443 }
444 }
445
446 if probe.has("wp_presentation") {
448 match map_and_measure(&connection, &mut queue, &mut probe) {
449 Ok(()) => {}
450 Err(reason) => failures.push(reason),
451 }
452 }
453
454 if let Ok(seconds) = std::env::var("WL_PROBE_HOLD")
457 && let Ok(seconds) = seconds.trim().parse::<u64>()
458 {
459 println!();
460 println!("holding the bars up for {seconds}s — capture the compositor now");
461 let until = std::time::Instant::now() + Duration::from_secs(seconds);
462 while std::time::Instant::now() < until {
463 if queue.roundtrip(&mut probe).is_err() {
464 break;
465 }
466 std::thread::sleep(Duration::from_millis(50));
467 }
468 if probe.pointer_at.is_empty() {
473 println!("the pointer never entered a bar");
474 } else {
475 let mut seen: Vec<String> = Vec::new();
476 for (x, y) in &probe.pointer_at {
477 let at = format!("{x:.0},{y:.0}");
478 if !seen.contains(&at) {
479 seen.push(at);
480 }
481 }
482 println!("pointer, in the bar's own coordinates: {}", seen.join(" "));
483 }
484 }
485
486 println!();
487 if failures.is_empty() {
488 println!("wl-probe: everything asked for was answered");
489 } else {
490 for failure in &failures {
491 println!("wl-probe: FAIL — {failure}");
492 }
493 std::process::exit(1);
494 }
495}
496
497fn show_windows(
504 connection: &Connection,
505 queue: &mut wayland_client::EventQueue<Probe>,
506 probe: &mut Probe,
507 seconds: u64,
508) {
509 let handle = queue.handle();
510 let (Some(compositor), Some(shm), Some(wm_base)) = (
511 probe.compositor.clone(),
512 probe.shm.clone(),
513 probe.wm_base.clone(),
514 ) else {
515 eprintln!("wl-probe: the compositor would not hand over the basics");
516 return;
517 };
518
519 let mut windows = Vec::new();
520 for (index, own_frame) in [(0, false), (1, true)] {
521 let surface = compositor.create_surface(&handle, ());
522 let xdg = wm_base.get_xdg_surface(&surface, &handle, ());
523 let toplevel = xdg.get_toplevel(&handle, ());
524 toplevel.set_title(if own_frame {
525 "client-side — draws its own frame".to_owned()
526 } else {
527 "server-side — the compositor's frame".to_owned()
528 });
529 toplevel.set_app_id(format!("wl-probe-{index}"));
530
531 let decoration = probe.decorations.as_ref().map(|manager| {
534 let decoration = manager.get_toplevel_decoration(&toplevel, &handle, ());
535 decoration.set_mode(if own_frame {
536 DecorationMode::ClientSide
537 } else {
538 DecorationMode::ServerSide
539 });
540 decoration
541 });
542 surface.commit();
543 windows.push((surface, toplevel, decoration));
544 }
545
546 for _ in 0..60 {
549 if settle(connection, queue, probe).is_err() {
550 return;
551 }
552 if probe.configured {
553 break;
554 }
555 }
556
557 for (surface, _, _) in &windows {
560 let buffer = solid_buffer(&shm, &handle, 520, 360);
561 surface.attach(Some(&buffer), 0, 0);
562 surface.damage(0, 0, i32::MAX, i32::MAX);
563 surface.commit();
564 }
565 for _ in 0..10 {
566 let _ = settle(connection, queue, probe);
567 }
568 println!("two windows up: one server-side, one client-side");
569
570 let fullscreen = std::env::var_os("WL_PROBE_FULLSCREEN").is_some();
571 let until = std::time::Instant::now() + Duration::from_secs(seconds);
572 let mut asked = false;
573 let start = std::time::Instant::now();
574 while std::time::Instant::now() < until {
575 if fullscreen && !asked && start.elapsed() >= Duration::from_secs(1) {
576 if let Some((_, toplevel, _)) = windows.first() {
577 println!("asking for fullscreen");
578 toplevel.set_fullscreen(None);
579 }
580 asked = true;
581 }
582 if queue.roundtrip(probe).is_err() {
584 break;
585 }
586 std::thread::sleep(Duration::from_millis(30));
587 }
588}
589
590fn show_popup(
594 connection: &Connection,
595 queue: &mut wayland_client::EventQueue<Probe>,
596 probe: &mut Probe,
597 seconds: u64,
598) {
599 let handle = queue.handle();
600 let (Some(compositor), Some(shm), Some(wm_base)) = (
601 probe.compositor.clone(),
602 probe.shm.clone(),
603 probe.wm_base.clone(),
604 ) else {
605 eprintln!("wl-probe: the compositor would not hand over the basics");
606 return;
607 };
608 let surface = compositor.create_surface(&handle, ());
609 let xdg = wm_base.get_xdg_surface(&surface, &handle, ());
610 let toplevel = xdg.get_toplevel(&handle, ());
611 toplevel.set_title("menu".to_owned());
612 toplevel.set_app_id("wl-probe-popup".to_owned());
613 surface.commit();
614 for _ in 0..60 {
615 if settle(connection, queue, probe).is_err() {
616 return;
617 }
618 if probe.configured {
619 break;
620 }
621 }
622 let window = tinted_buffer(&shm, &handle, 520, 360, [0xc0, 0xa0, 0x20, 0xff]);
623 surface.attach(Some(&window), 0, 0);
624 surface.damage(0, 0, i32::MAX, i32::MAX);
625 surface.commit();
626 let _ = settle(connection, queue, probe);
627
628 let menu = compositor.create_surface(&handle, ());
629 let menu_xdg = wm_base.get_xdg_surface(&menu, &handle, ());
630 let positioner = wm_base.create_positioner(&handle, ());
631 positioner.set_size(160, 120);
632 positioner.set_anchor_rect(500, 340, 1, 1);
633 positioner.set_anchor(xdg_positioner::Anchor::TopLeft);
634 positioner.set_gravity(xdg_positioner::Gravity::BottomRight);
635 let _popup = menu_xdg.get_popup(Some(&xdg), &positioner, &handle, ());
636 probe.configured = false;
637 menu.commit();
638 for _ in 0..60 {
639 if settle(connection, queue, probe).is_err() {
640 return;
641 }
642 if probe.configured {
643 break;
644 }
645 }
646 let colour = tinted_buffer(&shm, &handle, 160, 120, [0x40, 0x20, 0xd0, 0xff]);
647 menu.attach(Some(&colour), 0, 0);
648 menu.damage(0, 0, i32::MAX, i32::MAX);
649 menu.commit();
650 println!("a window and its menu are up");
651 let until = std::time::Instant::now() + Duration::from_secs(seconds);
652 while std::time::Instant::now() < until {
653 if queue.roundtrip(probe).is_err() {
654 break;
655 }
656 std::thread::sleep(Duration::from_millis(30));
657 }
658}
659
660fn take_a_shot(
675 connection: &Connection,
676 queue: &mut wayland_client::EventQueue<Probe>,
677 probe: &mut Probe,
678 region: Option<(i32, i32, i32, i32)>,
679) -> Result<(), String> {
680 let handle = queue.handle();
681 let (Some(shm), Some(screencopy)) = (probe.shm.clone(), probe.screencopy.clone()) else {
682 return Err("zwlr_screencopy_manager_v1 is advertised and would not bind".to_owned());
683 };
684 let Some(screen) = probe.outputs.first() else {
685 return Err("no outputs to capture".to_owned());
686 };
687 let (output, name) = (screen.output.clone(), screen.name.clone());
688
689 println!();
690 probe.shot = Some(Shot::default());
691 let frame = match region {
695 None => screencopy.capture_output(0, &output, &handle, ()),
696 Some((x, y, w, h)) => screencopy.capture_output_region(0, &output, x, y, w, h, &handle, ()),
697 };
698
699 for turn in 0..50 {
700 if turn == 10 {
701 eprintln!("wl-probe: still waiting to be told what buffer to allocate…");
702 }
703 settle(connection, queue, probe)?;
704 if probe.shot.as_ref().is_some_and(|shot| shot.described) {
705 break;
706 }
707 }
708
709 let (format, width, height, stride) = {
710 let shot = probe.shot.as_ref().ok_or("the capture vanished")?;
711 if !shot.described {
712 return Err("the compositor never said what buffer a capture needs".to_owned());
713 }
714 (
715 shot.format.ok_or("a capture with no format")?,
716 shot.width,
717 shot.height,
718 shot.stride,
719 )
720 };
721 println!("capture of {name}: {width}x{height}, stride {stride}, format {format}");
722
723 if width == 0 || height == 0 {
724 return Err("a capture of nothing".to_owned());
725 }
726 if stride < width * 4 {
727 return Err(format!(
728 "a stride of {stride} cannot hold {width} pixels of four bytes"
729 ));
730 }
731 let scale = probe
736 .outputs
737 .first()
738 .map_or(1, |screen| screen.scale.max(1));
739 let expected = match region {
740 None => u32::try_from(screen_pixels(probe)).unwrap_or(0),
741 Some((_, _, w, _)) => u32::try_from(w.max(0) * scale).unwrap_or(0),
742 };
743 if width != expected {
744 return Err(format!(
745 "a capture of {name} is {width} wide, expected {expected} device pixels"
746 ));
747 }
748
749 let length = (stride * height) as usize;
751 let file = tempfile_rs();
752 file.set_len(length as u64)
753 .unwrap_or_else(|err| panic!("wl-probe: could not size the capture pool: {err}"));
754 let pool = shm.create_pool(file.as_fd(), length as i32, &handle, ());
755 let buffer = pool.create_buffer(
756 0,
757 width as i32,
758 height as i32,
759 stride as i32,
760 wl_shm::Format::try_from(format).unwrap_or(wl_shm::Format::Xrgb8888),
761 &handle,
762 (),
763 );
764
765 frame.copy(&buffer);
766 for turn in 0..100 {
767 if turn == 20 {
768 eprintln!("wl-probe: still waiting for the capture to be filled…");
769 }
770 settle(connection, queue, probe)?;
771 let shot = probe.shot.as_ref().ok_or("the capture vanished")?;
772 if shot.ready || shot.failed {
773 break;
774 }
775 }
776
777 let shot = probe.shot.as_ref().ok_or("the capture vanished")?;
778 if shot.failed {
779 return Err(format!("the compositor refused to capture {name}"));
780 }
781 if !shot.ready {
782 return Err(format!(
783 "asked for a capture of {name} and was never told it was done"
784 ));
785 }
786 println!(" ok filled, flags {:#04b}", shot.flags);
787
788 let pixels = std::fs::read(
790 std::path::Path::new("/proc/self/fd").join(file.as_fd().as_raw_fd().to_string()),
791 )
792 .unwrap_or_default();
793 let pixels = if pixels.len() >= length {
794 pixels
795 } else {
796 return Err("could not read the capture back to look at it".to_owned());
799 };
800
801 if let Ok(path) = std::env::var("WL_PROBE_SHOT") {
802 write_ppm(&path, width, height, stride, &pixels);
803 println!(" wrote {path}");
804 }
805
806 let lit = pixels
810 .as_chunks::<4>()
811 .0
812 .iter()
813 .filter(|pixel| pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0)
814 .count();
815 if lit * 2 <= (width * height) as usize {
816 return Err(format!(
817 "a capture of {name} came back mostly zeros -- the copy went somewhere else"
818 ));
819 }
820
821 let mut seen = std::collections::HashSet::new();
825 for row in 0..height as usize {
826 let start = row * stride as usize;
827 for x in (0..width as usize).step_by(7) {
828 let at = start + x * 4;
829 if let Some(pixel) = pixels.get(at..at + 3) {
830 seen.insert([pixel[0], pixel[1], pixel[2]]);
831 }
832 }
833 if seen.len() > 8 {
834 break;
835 }
836 }
837 println!(" ok {} distinct colours sampled", seen.len());
838 if seen.len() < 2 {
839 return Err(format!(
840 "a capture of {name} is one flat colour -- nothing was drawn into it"
841 ));
842 }
843
844 fn band(pixels: &[u8], width: u32, height: u32, stride: u32, from: usize) -> usize {
855 let mut found = 0;
856 for row in from..(from + 8).min(height as usize) {
857 let start = row * stride as usize;
858 for x in (0..width as usize).step_by(5) {
859 let Some(pixel) = pixels.get(start + x * 4..start + x * 4 + 3) else {
860 continue;
861 };
862 if usize::from(pixel[0]) > usize::from(pixel[2]) + 40 {
867 found += 1;
868 }
869 }
870 }
871 found
872 }
873 let (top, bottom) = (
874 band(&pixels, width, height, stride, 2),
875 band(&pixels, width, height, stride, height as usize - 10),
876 );
877 println!(" ok bar pixels: {top} near the top, {bottom} near the bottom");
878 match region {
879 None if top <= bottom => Err(format!(
880 "a capture of {name} has the top-anchored bar at the bottom -- the rows are \
881 upside down ({top} bar pixels at the top, {bottom} at the bottom)"
882 )),
883 Some((_, y, _, _)) if y > 64 && top > 0 => Err(format!(
886 "a capture of {name} from y={y} contains the bar, which is at the top of the \
887 screen -- the region offset was ignored"
888 )),
889 _ => Ok(()),
890 }
891}
892
893fn screen_pixels(probe: &Probe) -> i32 {
895 probe.outputs.first().map_or(0, |screen| screen.width)
896}
897
898fn write_ppm(path: &str, width: u32, height: u32, stride: u32, pixels: &[u8]) {
900 let mut out = format!("P6\n{width} {height}\n255\n").into_bytes();
901 for row in 0..height as usize {
902 let start = row * stride as usize;
903 for x in 0..width as usize {
904 let at = start + x * 4;
905 let (Some(b), Some(g), Some(r)) =
907 (pixels.get(at), pixels.get(at + 1), pixels.get(at + 2))
908 else {
909 return;
910 };
911 out.extend_from_slice(&[*r, *g, *b]);
912 }
913 }
914 let _ = std::fs::write(path, out);
915}
916
917fn anchor_a_bar(
929 connection: &Connection,
930 queue: &mut wayland_client::EventQueue<Probe>,
931 probe: &mut Probe,
932) -> Result<(), String> {
933 let handle = queue.handle();
934 let (Some(compositor), Some(shm), Some(layer_shell)) = (
935 probe.compositor.clone(),
936 probe.shm.clone(),
937 probe.layer_shell.clone(),
938 ) else {
939 return Err("zwlr_layer_shell_v1 is advertised and would not bind".to_owned());
940 };
941 if probe.outputs.is_empty() {
942 return Err("no outputs to put a bar on".to_owned());
943 }
944
945 println!();
946 let only = std::env::var("WL_PROBE_BAR").ok();
950 let screens: Vec<(WlOutput, String, i32)> = probe
955 .outputs
956 .iter()
957 .filter(|screen| only.as_deref().is_none_or(|only| only == screen.name))
958 .map(|screen| {
959 (
960 screen.output.clone(),
961 screen.name.clone(),
962 screen.logical().0,
963 )
964 })
965 .collect();
966 if screens.is_empty() {
967 return Err(format!(
968 "no output called {:?} — this compositor has {:?}",
969 only.unwrap_or_default(),
970 probe
971 .outputs
972 .iter()
973 .map(|screen| screen.name.clone())
974 .collect::<Vec<_>>()
975 ));
976 }
977
978 let mut surfaces = Vec::new();
979 for (output, name, _) in &screens {
980 let surface = compositor.create_surface(&handle, ());
981 let bar = layer_shell.get_layer_surface(
986 &surface,
987 Some(output),
988 Layer::Top,
989 format!("wl-probe-bar-{name}"),
990 &handle,
991 name.clone(),
992 );
993 bar.set_anchor(Anchor::Top | Anchor::Left | Anchor::Right);
994 bar.set_size(0, BAR as u32);
995 bar.set_exclusive_zone(BAR);
998 surface.commit();
999 surfaces.push((surface, bar, name.clone()));
1000 }
1001
1002 for turn in 0..50 {
1003 if turn == 10 {
1004 eprintln!("wl-probe: still waiting for the bars to be configured…");
1005 }
1006 settle(connection, queue, probe)?;
1007 if probe.bars.len() >= screens.len() {
1008 break;
1009 }
1010 }
1011
1012 for (surface, _, _) in &surfaces {
1015 let buffer = solid_buffer(&shm, &handle, 3840, BAR);
1016 surface.attach(Some(&buffer), 0, 0);
1017 surface.damage(0, 0, i32::MAX, i32::MAX);
1018 surface.commit();
1019 }
1020 settle(connection, queue, probe)?;
1021
1022 let mut wrong = Vec::new();
1023 for (_, name, width) in &screens {
1024 match probe.bars.iter().find(|(on, _, _)| on == name) {
1025 Some((_, configured, height)) => {
1026 let ok = i64::from(*configured) == i64::from(*width);
1027 println!(
1028 " {} bar on {name}: configured {configured}x{height}, monitor is \
1029 {width} logical wide",
1030 if ok { "ok " } else { "MISMATCH" }
1031 );
1032 if !ok {
1033 wrong.push(format!(
1037 "a bar that named {name} was configured {configured} wide, but {name} \
1038 is {width} logical — it landed on another monitor"
1039 ));
1040 }
1041 if i64::from(*height) != i64::from(BAR) {
1042 wrong.push(format!(
1043 "a bar on {name} asked for {BAR} tall and was configured {height}"
1044 ));
1045 }
1046 }
1047 None => wrong.push(format!("a bar that named {name} was never configured")),
1048 }
1049 }
1050
1051 probe
1053 .bars_alive
1054 .extend(surfaces.into_iter().map(|(surface, bar, _)| (surface, bar)));
1055
1056 if let Some(first) = wrong.into_iter().next() {
1057 return Err(first);
1058 }
1059 Ok(())
1060}
1061
1062fn map_and_measure(
1064 connection: &Connection,
1065 queue: &mut wayland_client::EventQueue<Probe>,
1066 probe: &mut Probe,
1067) -> Result<(), String> {
1068 let handle = queue.handle();
1069 let (Some(compositor), Some(shm), Some(wm_base), Some(presentation)) = (
1070 probe.compositor.clone(),
1071 probe.shm.clone(),
1072 probe.wm_base.clone(),
1073 probe.presentation.clone(),
1074 ) else {
1075 return Err("the compositor advertised globals it then refused to bind".to_owned());
1076 };
1077
1078 println!();
1079 match probe.clock {
1080 Some(1) => println!("presentation clock: 1 (CLOCK_MONOTONIC)"),
1083 Some(other) => println!("presentation clock: {other} — not CLOCK_MONOTONIC"),
1084 None => return Err("wp_presentation never said which clock it uses".to_owned()),
1085 }
1086
1087 let surface = compositor.create_surface(&handle, ());
1088 let xdg = wm_base.get_xdg_surface(&surface, &handle, ());
1089 let toplevel = xdg.get_toplevel(&handle, ());
1090 toplevel.set_title("wl-probe".to_owned());
1091 surface.commit();
1092 probe.surface = Some(surface.clone());
1093
1094 for turn in 0..50 {
1103 if turn == 10 {
1107 eprintln!("wl-probe: still waiting to be configured…");
1108 }
1109 settle(connection, queue, probe)?;
1110 if probe.configured {
1111 break;
1112 }
1113 }
1114 if !probe.configured {
1115 return Err("the compositor never configured the window".to_owned());
1116 }
1117
1118 let buffer = solid_buffer(&shm, &handle, 320, 200);
1119
1120 for _ in 0..FRAMES {
1121 presentation.feedback(&surface, &handle, ());
1124 surface.attach(Some(&buffer), 0, 0);
1125 surface.damage(0, 0, i32::MAX, i32::MAX);
1126 surface.commit();
1127 for _ in 0..20 {
1130 settle(connection, queue, probe)?;
1131 if probe.presented.len() + probe.discarded >= FRAMES {
1132 break;
1133 }
1134 }
1135 }
1136
1137 println!(
1138 "frames asked about: {FRAMES} presented: {} discarded: {}",
1139 probe.presented.len(),
1140 probe.discarded
1141 );
1142 for frame in &probe.presented {
1143 println!(
1144 " at {:?} refresh {:?} seq {} flags {:#04b}",
1145 frame.when, frame.refresh, frame.sequence, frame.flags
1146 );
1147 }
1148
1149 if probe.presented.is_empty() {
1150 return Err(format!(
1151 "asked about {FRAMES} frames and was told about none — \
1152 the global is advertised and answers nothing"
1153 ));
1154 }
1155
1156 let ordered = probe
1159 .presented
1160 .windows(2)
1161 .all(|pair| pair[1].when >= pair[0].when);
1162 if !ordered {
1163 return Err("presentation timestamps went backwards".to_owned());
1164 }
1165
1166 if let Some(first) = probe.presented.first()
1170 && probe.clock == Some(1)
1171 && first.when < Duration::from_secs(60)
1172 {
1173 return Err(format!(
1174 "the first timestamp is {:?}, which is not CLOCK_MONOTONIC \
1175 — that clock counts from boot",
1176 first.when
1177 ));
1178 }
1179
1180 Ok(())
1181}
1182
1183fn settle(
1189 connection: &Connection,
1190 queue: &mut wayland_client::EventQueue<Probe>,
1191 probe: &mut Probe,
1192) -> Result<(), String> {
1193 connection.flush().ok();
1194 queue
1195 .roundtrip(probe)
1196 .map_err(|err| format!("the compositor stopped talking: {err}"))?;
1197 std::thread::sleep(Duration::from_millis(20));
1198 Ok(())
1199}
1200
1201fn solid_buffer(
1203 shm: &wl_shm::WlShm,
1204 handle: &QueueHandle<Probe>,
1205 width: i32,
1206 height: i32,
1207) -> WlBuffer {
1208 tinted_buffer(shm, handle, width, height, [0x80, 0x50, 0x20, 0xff])
1211}
1212
1213fn tinted_buffer(
1219 shm: &wl_shm::WlShm,
1220 handle: &QueueHandle<Probe>,
1221 width: i32,
1222 height: i32,
1223 pixel: [u8; 4],
1224) -> WlBuffer {
1225 let stride = width * 4;
1226 let size = stride * height;
1227 let file = tempfile(size as usize, pixel);
1228 let pool = shm.create_pool(file.as_fd(), size, handle, ());
1229 pool.create_buffer(
1230 0,
1231 width,
1232 height,
1233 stride,
1234 wl_shm::Format::Argb8888,
1235 handle,
1236 (),
1237 )
1238}
1239
1240fn tempfile(size: usize, pixel: [u8; 4]) -> std::fs::File {
1242 use std::io::{Seek as _, Write as _};
1243 let mut file = tempfile_rs();
1244 let row: Vec<u8> = pixel.iter().copied().cycle().take(size).collect();
1245 file.write_all(&row).ok();
1246 file.flush().ok();
1247 file.seek(std::io::SeekFrom::Start(0)).ok();
1248 file
1249}
1250
1251fn tempfile_rs() -> std::fs::File {
1252 let dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_owned());
1255 let path = format!("{dir}/wl-probe-{}", std::process::id());
1256 let file = std::fs::OpenOptions::new()
1257 .read(true)
1258 .write(true)
1259 .create(true)
1260 .truncate(true)
1261 .open(&path)
1262 .unwrap_or_else(|err| panic!("wl-probe: could not make a buffer file: {err}"));
1263 std::fs::remove_file(&path).ok();
1264 file
1265}
1266
1267fn watch_keyboard(
1276 connection: &Connection,
1277 queue: &mut wayland_client::EventQueue<Probe>,
1278 probe: &mut Probe,
1279 seconds: u64,
1280) -> Result<(), String> {
1281 let handle = queue.handle();
1282 let (Some(compositor), Some(shm), Some(wm_base)) = (
1283 probe.compositor.clone(),
1284 probe.shm.clone(),
1285 probe.wm_base.clone(),
1286 ) else {
1287 return Err("no compositor, shm or xdg_wm_base".to_owned());
1288 };
1289
1290 let surface = compositor.create_surface(&handle, ());
1291 let xdg = wm_base.get_xdg_surface(&surface, &handle, ());
1292 let toplevel = xdg.get_toplevel(&handle, ());
1293 toplevel.set_title("wl-probe keyboard".to_owned());
1294 surface.commit();
1295 for _ in 0..50 {
1296 settle(connection, queue, probe)?;
1297 if probe.configured {
1298 break;
1299 }
1300 }
1301 let buffer = solid_buffer(&shm, &handle, 320, 200);
1302 surface.attach(Some(&buffer), 0, 0);
1303 surface.damage(0, 0, i32::MAX, i32::MAX);
1304 surface.commit();
1305 settle(connection, queue, probe)?;
1306
1307 println!();
1308 println!("layouts: {}", probe.layouts.join(", "));
1309 let started = probe.active_layout;
1310 println!("active at the start: {:?}", started);
1311 println!("watching for {seconds}s — switch the layout now");
1312
1313 let mut seen: Vec<u32> = started.into_iter().collect();
1314 let until = std::time::Instant::now() + Duration::from_secs(seconds);
1315 while std::time::Instant::now() < until {
1316 settle(connection, queue, probe)?;
1317 if let Some(active) = probe.active_layout
1318 && !seen.contains(&active)
1319 {
1320 let name = probe
1321 .layouts
1322 .get(active as usize)
1323 .cloned()
1324 .unwrap_or_else(|| format!("group {active}"));
1325 println!("told the active layout is now {active} ({name})");
1326 seen.push(active);
1327 }
1328 std::thread::sleep(Duration::from_millis(40));
1329 }
1330
1331 toplevel.destroy();
1332 xdg.destroy();
1333 surface.destroy();
1334 if seen.len() < 2 {
1335 return Err(format!(
1336 "the active layout never changed as far as this client was told (saw {seen:?})"
1337 ));
1338 }
1339 Ok(())
1340}
1341
1342fn watch_idle(
1357 connection: &Connection,
1358 queue: &mut wayland_client::EventQueue<Probe>,
1359 probe: &mut Probe,
1360) -> Result<(), String> {
1361 let handle = queue.handle();
1362 let (Some(compositor), Some(shm), Some(wm_base), Some(notifier), Some(inhibit_manager)) = (
1363 probe.compositor.clone(),
1364 probe.shm.clone(),
1365 probe.wm_base.clone(),
1366 probe.idle_notifier.clone(),
1367 probe.idle_inhibit.clone(),
1368 ) else {
1369 return Err("the idle protocols are advertised and would not bind".to_owned());
1370 };
1371 let Some(seat) = probe.seat.clone() else {
1372 return Err("no wl_seat, so there is nothing to be idle on".to_owned());
1373 };
1374
1375 let timeout_ms: u32 = 700;
1378 let timeout = Duration::from_millis(u64::from(timeout_ms));
1379
1380 println!();
1381 let surface = compositor.create_surface(&handle, ());
1385 let xdg = wm_base.get_xdg_surface(&surface, &handle, ());
1386 let toplevel = xdg.get_toplevel(&handle, ());
1387 toplevel.set_title("wl-probe idle".to_owned());
1388 surface.commit();
1389 for _ in 0..50 {
1390 settle(connection, queue, probe)?;
1391 if probe.configured {
1392 break;
1393 }
1394 }
1395 if !probe.configured {
1396 return Err("the compositor never configured the window".to_owned());
1397 }
1398 let buffer = solid_buffer(&shm, &handle, 320, 200);
1399 surface.attach(Some(&buffer), 0, 0);
1400 surface.damage(0, 0, i32::MAX, i32::MAX);
1401 surface.commit();
1402 settle(connection, queue, probe)?;
1403
1404 let notification = notifier.get_idle_notification(timeout_ms, &seat, &handle, ());
1405 probe.idle_events.clear();
1406
1407 let wait = |probe: &mut Probe,
1408 queue: &mut wayland_client::EventQueue<Probe>,
1409 want: usize,
1410 how_long: Duration|
1411 -> Result<(), String> {
1412 let until = std::time::Instant::now() + how_long;
1413 while std::time::Instant::now() < until && probe.idle_events.len() < want {
1414 settle(connection, queue, probe)?;
1415 std::thread::sleep(Duration::from_millis(20));
1416 }
1417 Ok(())
1418 };
1419
1420 wait(probe, queue, 1, timeout * 4)?;
1422 if probe.idle_events.first() != Some(&"idled") {
1423 return Err(format!(
1424 "waited {}ms past a {timeout_ms}ms timeout and was never told the seat went idle \
1425 (events: {:?})",
1426 timeout.as_millis() * 4,
1427 probe.idle_events
1428 ));
1429 }
1430 println!("idled after {timeout_ms}ms with nobody at the keyboard");
1431
1432 let inhibitor = inhibit_manager.create_inhibitor(&surface, &handle, ());
1436 wait(probe, queue, 2, timeout * 2)?;
1437 if probe.idle_events.get(1) != Some(&"resumed") {
1438 return Err(format!(
1439 "an inhibitor was taken on a visible window and the seat stayed idle (events: {:?})",
1440 probe.idle_events
1441 ));
1442 }
1443 println!("resumed as soon as a visible window asked to stay awake");
1444
1445 wait(probe, queue, 3, timeout * 3)?;
1448 if probe.idle_events.len() != 2 {
1449 return Err(format!(
1450 "the inhibitor was held for {}ms and the seat went idle anyway (events: {:?})",
1451 timeout.as_millis() * 3,
1452 probe.idle_events
1453 ));
1454 }
1455 println!(
1456 "held awake for {}ms while the inhibitor was up",
1457 timeout.as_millis() * 3
1458 );
1459
1460 inhibitor.destroy();
1463 wait(probe, queue, 3, timeout * 4)?;
1464 if probe.idle_events.get(2) != Some(&"idled") {
1465 return Err(format!(
1466 "the inhibitor was dropped and the seat never went idle again (events: {:?})",
1467 probe.idle_events
1468 ));
1469 }
1470 println!("idled again once the inhibitor was dropped");
1471
1472 if std::env::var_os("WL_PROBE_IDLE_LOCK").is_some()
1477 && let Some(manager) = probe.session_lock.clone()
1478 {
1479 let inhibitor = inhibit_manager.create_inhibitor(&surface, &handle, ());
1480 wait(probe, queue, 4, timeout * 2)?;
1481 if probe.idle_events.get(3) != Some(&"resumed") {
1482 return Err(format!(
1483 "took an inhibitor again and the seat stayed idle (events: {:?})",
1484 probe.idle_events
1485 ));
1486 }
1487
1488 let lock = manager.lock(&handle, ());
1489 settle(connection, queue, probe)?;
1490 if !probe.locked {
1491 return Err("the compositor did not confirm the lock".to_owned());
1492 }
1493 wait(probe, queue, 5, timeout * 4)?;
1497 let idled_behind_the_lock = probe.idle_events.get(4) == Some(&"idled");
1498 lock.unlock_and_destroy();
1499 inhibitor.destroy();
1500 connection.flush().ok();
1501 settle(connection, queue, probe)?;
1502 if !idled_behind_the_lock {
1503 return Err(format!(
1504 "a window kept holding the machine awake behind a lock screen (events: {:?})",
1505 probe.idle_events
1506 ));
1507 }
1508 println!("idled behind a lock screen even with the inhibitor still held");
1509 }
1510
1511 if let Ok(seconds) = std::env::var("WL_PROBE_IDLE_HIDDEN")
1517 && let Ok(seconds) = seconds.trim().parse::<u64>()
1518 {
1519 let inhibitor = inhibit_manager.create_inhibitor(&surface, &handle, ());
1520 let before = probe.idle_events.len();
1521 wait(probe, queue, before + 1, timeout * 2)?;
1522 if probe.idle_events.get(before) != Some(&"resumed") {
1523 return Err(format!(
1524 "took an inhibitor again and the seat stayed idle (events: {:?})",
1525 probe.idle_events
1526 ));
1527 }
1528 println!("holding an inhibitor for {seconds}s — hide this window now");
1529 wait(probe, queue, before + 2, Duration::from_secs(seconds))?;
1530 let idled_while_hidden = probe.idle_events.get(before + 1) == Some(&"idled");
1531 inhibitor.destroy();
1532 connection.flush().ok();
1533 if !idled_while_hidden {
1534 return Err(format!(
1535 "a window moved off every screen kept holding the machine awake \
1536 (events: {:?})",
1537 probe.idle_events
1538 ));
1539 }
1540 println!("idled once the window was no longer on any screen");
1541 }
1542
1543 notification.destroy();
1544 toplevel.destroy();
1545 xdg.destroy();
1546 surface.destroy();
1547 Ok(())
1548}
1549
1550fn lock_the_session(
1563 connection: &Connection,
1564 queue: &mut wayland_client::EventQueue<Probe>,
1565 probe: &mut Probe,
1566 seconds: u64,
1567) -> Result<(), String> {
1568 let handle = queue.handle();
1569 let (Some(compositor), Some(shm), Some(manager)) = (
1570 probe.compositor.clone(),
1571 probe.shm.clone(),
1572 probe.session_lock.clone(),
1573 ) else {
1574 return Err("ext_session_lock_manager_v1 is not advertised".to_owned());
1575 };
1576 if probe.outputs.is_empty() {
1577 return Err("no outputs to lock".to_owned());
1578 }
1579
1580 println!();
1581 let lock = manager.lock(&handle, ());
1582 settle(connection, queue, probe)?;
1583 if probe.lock_refused {
1584 return Err("the compositor refused the lock (finished)".to_owned());
1585 }
1586 if !probe.locked {
1587 return Err("the compositor neither confirmed nor refused the lock".to_owned());
1588 }
1589 println!("locked: the compositor confirmed before any surface existed");
1590
1591 let skip = std::env::var("WL_PROBE_LOCK_SKIP").ok();
1597 let screens: Vec<(WlOutput, String)> = probe
1598 .outputs
1599 .iter()
1600 .filter(|screen| skip.as_deref() != Some(screen.name.as_str()))
1601 .map(|screen| (screen.output.clone(), screen.name.clone()))
1602 .collect();
1603
1604 let mut surfaces = Vec::new();
1605 for (output, name) in &screens {
1606 let surface = compositor.create_surface(&handle, ());
1607 let locked = lock.get_lock_surface(&surface, output, &handle, name.clone());
1608 surfaces.push((surface, locked, name.clone()));
1611 }
1612
1613 for turn in 0..50 {
1614 if turn == 10 {
1615 eprintln!("wl-probe: still waiting for the lock surfaces to be configured…");
1616 }
1617 settle(connection, queue, probe)?;
1618 if probe.lock_surfaces.len() >= screens.len() {
1619 break;
1620 }
1621 }
1622 if probe.lock_surfaces.len() < screens.len() {
1623 return Err(format!(
1624 "{} of {} lock surfaces were configured",
1625 probe.lock_surfaces.len(),
1626 screens.len()
1627 ));
1628 }
1629
1630 for (surface, _, name) in &surfaces {
1631 let Some((width, height)) = probe
1632 .lock_surfaces
1633 .iter()
1634 .find(|(each, _, _)| each == name)
1635 .map(|(_, width, height)| (*width, *height))
1636 else {
1637 continue;
1638 };
1639 let buffer = tinted_buffer(
1641 &shm,
1642 &handle,
1643 width as i32,
1644 height as i32,
1645 [0x20, 0xc0, 0x40, 0xff],
1646 );
1647 surface.attach(Some(&buffer), 0, 0);
1648 surface.damage_buffer(0, 0, width as i32, height as i32);
1649 surface.commit();
1650 println!("lock surface on {name}: {width}x{height}");
1651 }
1652 if let Some(name) = skip.as_deref() {
1653 println!(
1654 "lock surface on {name}: none, deliberately — it should be blank, not the desktop"
1655 );
1656 }
1657
1658 settle(connection, queue, probe)?;
1659 println!("holding the lock for {seconds}s — the desktop must not be visible anywhere");
1660 let until = std::time::Instant::now() + Duration::from_secs(seconds);
1661 while std::time::Instant::now() < until {
1662 settle(connection, queue, probe)?;
1663 std::thread::sleep(Duration::from_millis(80));
1664 }
1665
1666 if std::env::var_os("WL_PROBE_LOCK_ABANDON").is_some() {
1670 println!("leaving without unlocking — the session must stay locked and blank");
1671 connection.flush().ok();
1672 return Ok(());
1673 }
1674 lock.unlock_and_destroy();
1675 connection.flush().ok();
1676 queue
1677 .roundtrip(probe)
1678 .map_err(|err| format!("the compositor stopped talking: {err}"))?;
1679 println!("unlocked");
1680 Ok(())
1681}
1682
1683impl Dispatch<ExtSessionLockV1, ()> for Probe {
1684 fn event(
1685 probe: &mut Self,
1686 _lock: &ExtSessionLockV1,
1687 event: ext_session_lock_v1::Event,
1688 _data: &(),
1689 _connection: &Connection,
1690 _handle: &QueueHandle<Self>,
1691 ) {
1692 match event {
1693 ext_session_lock_v1::Event::Locked => probe.locked = true,
1694 ext_session_lock_v1::Event::Finished => probe.lock_refused = true,
1699 _ => {}
1700 }
1701 }
1702}
1703
1704impl Dispatch<ExtSessionLockSurfaceV1, String> for Probe {
1705 fn event(
1706 probe: &mut Self,
1707 surface: &ExtSessionLockSurfaceV1,
1708 event: ext_session_lock_surface_v1::Event,
1709 name: &String,
1710 _connection: &Connection,
1711 _handle: &QueueHandle<Self>,
1712 ) {
1713 if let ext_session_lock_surface_v1::Event::Configure {
1714 serial,
1715 width,
1716 height,
1717 } = event
1718 {
1719 surface.ack_configure(serial);
1720 probe.lock_surfaces.retain(|(each, _, _)| each != name);
1721 probe.lock_surfaces.push((name.clone(), width, height));
1722 }
1723 }
1724}
1725
1726impl Dispatch<wl_registry::WlRegistry, ()> for Probe {
1727 fn event(
1728 state: &mut Self,
1729 registry: &wl_registry::WlRegistry,
1730 event: wl_registry::Event,
1731 (): &(),
1732 _: &Connection,
1733 handle: &QueueHandle<Self>,
1734 ) {
1735 let wl_registry::Event::Global {
1736 name,
1737 interface,
1738 version,
1739 } = event
1740 else {
1741 return;
1742 };
1743 state.globals.push((interface.clone(), version));
1744 match interface.as_str() {
1745 "wl_compositor" => {
1746 state.compositor = Some(registry.bind(name, version.min(5), handle, ()));
1747 }
1748 "wl_shm" => state.shm = Some(registry.bind(name, 1, handle, ())),
1749 "xdg_wm_base" => state.wm_base = Some(registry.bind(name, version.min(6), handle, ())),
1750 "wl_output" => {
1753 let output: WlOutput = registry.bind(name, version.min(4), handle, ());
1754 state.outputs.push(Screen {
1755 output,
1756 name: String::new(),
1757 width: 0,
1758 height: 0,
1759 scale: 1,
1760 });
1761 }
1762 "zwlr_layer_shell_v1" => {
1763 state.layer_shell = Some(registry.bind(name, version.min(4), handle, ()));
1764 }
1765 "zwlr_screencopy_manager_v1" => {
1766 state.screencopy = Some(registry.bind(name, version.min(3), handle, ()));
1767 }
1768 "ext_session_lock_manager_v1" => {
1769 state.session_lock = Some(registry.bind(name, version.min(1), handle, ()));
1770 }
1771 "ext_idle_notifier_v1" => {
1772 state.idle_notifier = Some(registry.bind(name, version.min(2), handle, ()));
1773 }
1774 "zwp_idle_inhibit_manager_v1" => {
1775 state.idle_inhibit = Some(registry.bind(name, version.min(1), handle, ()));
1776 }
1777 "wl_seat" => {
1778 let seat: WlSeat = registry.bind(name, version.min(5), handle, ());
1779 seat.get_pointer(handle, ());
1780 seat.get_keyboard(handle, ());
1781 state.seat = Some(seat);
1782 }
1783 "zxdg_decoration_manager_v1" => {
1784 state.decorations = Some(registry.bind(name, version.min(1), handle, ()));
1785 }
1786 "wp_presentation" => {
1787 state.presentation = Some(registry.bind(name, version.min(1), handle, ()));
1788 }
1789 _ => {}
1790 }
1791 }
1792}
1793
1794impl Dispatch<WpPresentation, ()> for Probe {
1795 fn event(
1796 state: &mut Self,
1797 _: &WpPresentation,
1798 event: wp_presentation::Event,
1799 (): &(),
1800 _: &Connection,
1801 _: &QueueHandle<Self>,
1802 ) {
1803 if let wp_presentation::Event::ClockId { clk_id } = event {
1804 state.clock = Some(clk_id);
1805 }
1806 }
1807}
1808
1809impl Dispatch<WpPresentationFeedback, ()> for Probe {
1810 fn event(
1811 state: &mut Self,
1812 _: &WpPresentationFeedback,
1813 event: wp_presentation_feedback::Event,
1814 (): &(),
1815 _: &Connection,
1816 _: &QueueHandle<Self>,
1817 ) {
1818 match event {
1819 wp_presentation_feedback::Event::Presented {
1820 tv_sec_hi,
1821 tv_sec_lo,
1822 tv_nsec,
1823 refresh,
1824 seq_hi,
1825 seq_lo,
1826 flags,
1827 ..
1828 } => {
1829 let seconds = (u64::from(tv_sec_hi) << 32) | u64::from(tv_sec_lo);
1830 state.presented.push(Presented {
1831 when: Duration::new(seconds, tv_nsec),
1832 refresh: Duration::from_nanos(u64::from(refresh)),
1833 sequence: (u64::from(seq_hi) << 32) | u64::from(seq_lo),
1834 flags: u32::from(flags),
1835 });
1836 }
1837 wp_presentation_feedback::Event::Discarded => state.discarded += 1,
1838 _ => {}
1839 }
1840 }
1841}
1842
1843impl Dispatch<XdgWmBase, ()> for Probe {
1844 fn event(
1845 _: &mut Self,
1846 base: &XdgWmBase,
1847 event: xdg_wm_base::Event,
1848 (): &(),
1849 _: &Connection,
1850 _: &QueueHandle<Self>,
1851 ) {
1852 if let xdg_wm_base::Event::Ping { serial } = event {
1854 base.pong(serial);
1855 }
1856 }
1857}
1858
1859impl Dispatch<XdgSurface, ()> for Probe {
1860 fn event(
1861 state: &mut Self,
1862 xdg: &XdgSurface,
1863 event: xdg_surface::Event,
1864 (): &(),
1865 _: &Connection,
1866 _: &QueueHandle<Self>,
1867 ) {
1868 if let xdg_surface::Event::Configure { serial } = event {
1869 xdg.ack_configure(serial);
1870 state.configured = true;
1871 if let Some(surface) = state.surface.as_ref() {
1872 surface.commit();
1873 }
1874 }
1875 }
1876}
1877
1878delegate_noop!(Probe: ignore WlCompositor);
1879delegate_noop!(Probe: ignore WlSurface);
1880delegate_noop!(Probe: ignore wl_shm::WlShm);
1881delegate_noop!(Probe: ignore WlShmPool);
1882delegate_noop!(Probe: ignore WlBuffer);
1883delegate_noop!(Probe: ignore XdgToplevel);
1884delegate_noop!(Probe: ignore XdgPopup);
1885delegate_noop!(Probe: ignore XdgPositioner);
1886delegate_noop!(Probe: ignore ZwlrLayerShellV1);
1887delegate_noop!(Probe: ignore ZwlrScreencopyManagerV1);
1888delegate_noop!(Probe: ignore ExtSessionLockManagerV1);
1889delegate_noop!(Probe: ignore ExtIdleNotifierV1);
1890delegate_noop!(Probe: ignore ZwpIdleInhibitManagerV1);
1891delegate_noop!(Probe: ignore ZwpIdleInhibitorV1);
1892
1893impl Dispatch<ExtIdleNotificationV1, ()> for Probe {
1894 fn event(
1895 probe: &mut Self,
1896 _notification: &ExtIdleNotificationV1,
1897 event: ext_idle_notification_v1::Event,
1898 _data: &(),
1899 _connection: &Connection,
1900 _handle: &QueueHandle<Self>,
1901 ) {
1902 match event {
1903 ext_idle_notification_v1::Event::Idled => probe.idle_events.push("idled"),
1904 ext_idle_notification_v1::Event::Resumed => probe.idle_events.push("resumed"),
1905 _ => {}
1906 }
1907 }
1908}
1909delegate_noop!(Probe: ignore WlSeat);
1910
1911impl Dispatch<WlKeyboard, ()> for Probe {
1912 fn event(
1913 probe: &mut Self,
1914 _keyboard: &WlKeyboard,
1915 event: wl_keyboard::Event,
1916 _data: &(),
1917 _connection: &Connection,
1918 _handle: &QueueHandle<Self>,
1919 ) {
1920 match event {
1921 wl_keyboard::Event::Keymap { fd, size, .. } => {
1927 probe.layouts = layouts_in_keymap(fd, size as usize);
1928 }
1929 wl_keyboard::Event::Modifiers { group, .. } => probe.active_layout = Some(group),
1930 wl_keyboard::Event::RepeatInfo { rate, delay } => probe.repeat = Some((rate, delay)),
1931 _ => {}
1932 }
1933 }
1934}
1935
1936fn layouts_in_keymap(fd: std::os::fd::OwnedFd, size: usize) -> Vec<String> {
1944 use std::os::unix::fs::FileExt as _;
1951 let file = std::fs::File::from(fd);
1952 let mut bytes = vec![0u8; size];
1953 let read = file.read_at(&mut bytes, 0).unwrap_or(0);
1954 let mut text = String::new();
1955 bytes.truncate(read);
1956 while bytes.last() == Some(&0) {
1957 bytes.pop();
1958 }
1959 text.push_str(&String::from_utf8_lossy(&bytes));
1960
1961 if let Ok(path) = std::env::var("WL_PROBE_KEYMAP")
1966 && std::fs::write(&path, &text).is_ok()
1967 {
1968 println!("keymap written to {path} ({} bytes)", text.len());
1969 }
1970 layouts_in_keymap_text(&text)
1971}
1972
1973fn layouts_in_keymap_text(text: &str) -> Vec<String> {
1997 let mut layouts: Vec<(usize, String)> = Vec::new();
1998 let mut in_symbols = false;
1999 for line in text.lines() {
2000 let trimmed = line.trim();
2001 if trimmed.starts_with("xkb_") {
2002 in_symbols = trimmed.starts_with("xkb_symbols");
2003 continue;
2004 }
2005 if !in_symbols {
2006 continue;
2007 }
2008 let Some(rest) = trimmed.strip_prefix("name[") else {
2009 continue;
2010 };
2011 let Some((index, rest)) = rest.split_once(']') else {
2012 continue;
2013 };
2014 let Ok(index) = index.parse::<usize>() else {
2015 continue;
2016 };
2017 let Some(name) = rest.split('"').nth(1) else {
2018 continue;
2019 };
2020 if !layouts.iter().any(|(seen, _)| *seen == index) {
2021 layouts.push((index, name.to_owned()));
2022 }
2023 }
2024 layouts.sort_by_key(|(index, _)| *index);
2025 layouts.into_iter().map(|(_, name)| name).collect()
2026}
2027
2028impl Dispatch<WlPointer, ()> for Probe {
2029 fn event(
2030 probe: &mut Self,
2031 _pointer: &WlPointer,
2032 event: wl_pointer::Event,
2033 _data: &(),
2034 _connection: &Connection,
2035 _handle: &QueueHandle<Self>,
2036 ) {
2037 match event {
2043 wl_pointer::Event::Enter {
2044 surface_x,
2045 surface_y,
2046 ..
2047 }
2048 | wl_pointer::Event::Motion {
2049 surface_x,
2050 surface_y,
2051 ..
2052 } => probe.pointer_at.push((surface_x, surface_y)),
2053 _ => {}
2054 }
2055 }
2056}
2057delegate_noop!(Probe: ignore ZxdgDecorationManagerV1);
2058
2059impl Dispatch<ZxdgToplevelDecorationV1, ()> for Probe {
2060 fn event(
2061 _state: &mut Self,
2062 _decoration: &ZxdgToplevelDecorationV1,
2063 event: zxdg_toplevel_decoration_v1::Event,
2064 (): &(),
2065 _: &Connection,
2066 _: &QueueHandle<Self>,
2067 ) {
2068 if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event {
2069 println!(" decoration mode: {mode:?}");
2070 }
2071 }
2072}
2073
2074impl Dispatch<ZwlrScreencopyFrameV1, ()> for Probe {
2075 fn event(
2076 state: &mut Self,
2077 _frame: &ZwlrScreencopyFrameV1,
2078 event: zwlr_screencopy_frame_v1::Event,
2079 (): &(),
2080 _: &Connection,
2081 _: &QueueHandle<Self>,
2082 ) {
2083 let Some(shot) = state.shot.as_mut() else {
2084 return;
2085 };
2086 match event {
2087 zwlr_screencopy_frame_v1::Event::Buffer {
2088 format,
2089 width,
2090 height,
2091 stride,
2092 } => {
2093 shot.format = Some(format.into());
2094 shot.width = width;
2095 shot.height = height;
2096 shot.stride = stride;
2097 shot.described = true;
2100 }
2101 zwlr_screencopy_frame_v1::Event::BufferDone => shot.described = true,
2102 zwlr_screencopy_frame_v1::Event::Flags { flags } => shot.flags = flags.into(),
2103 zwlr_screencopy_frame_v1::Event::Ready { .. } => shot.ready = true,
2104 zwlr_screencopy_frame_v1::Event::Failed => shot.failed = true,
2105 _ => {}
2106 }
2107 }
2108}
2109
2110impl Dispatch<WlOutput, ()> for Probe {
2111 fn event(
2112 state: &mut Self,
2113 output: &WlOutput,
2114 event: wl_output::Event,
2115 (): &(),
2116 _: &Connection,
2117 _: &QueueHandle<Self>,
2118 ) {
2119 let Some(screen) = state
2120 .outputs
2121 .iter_mut()
2122 .find(|screen| &screen.output == output)
2123 else {
2124 return;
2125 };
2126 match event {
2127 wl_output::Event::Name { name } => screen.name = name,
2128 wl_output::Event::Mode {
2131 flags: WEnum::Value(flags),
2132 width,
2133 height,
2134 ..
2135 } if flags.contains(wl_output::Mode::Current) => {
2136 screen.width = width;
2137 screen.height = height;
2138 }
2139 wl_output::Event::Scale { factor } => screen.scale = factor,
2140 _ => {}
2141 }
2142 }
2143}
2144
2145impl Dispatch<ZwlrLayerSurfaceV1, String> for Probe {
2146 fn event(
2147 state: &mut Self,
2148 bar: &ZwlrLayerSurfaceV1,
2149 event: zwlr_layer_surface_v1::Event,
2150 on: &String,
2151 _: &Connection,
2152 _: &QueueHandle<Self>,
2153 ) {
2154 match event {
2155 zwlr_layer_surface_v1::Event::Configure {
2156 serial,
2157 width,
2158 height,
2159 } => {
2160 bar.ack_configure(serial);
2164 state.bars.push((on.clone(), width, height));
2165 }
2166 zwlr_layer_surface_v1::Event::Closed => {
2167 eprintln!("wl-probe: the compositor closed the bar on {on}");
2168 }
2169 _ => {}
2170 }
2171 }
2172}