Skip to main content

wl_probe/
main.rs

1//! A Wayland client that asks the compositor questions and prints the answers.
2//!
3//! Solium cannot test its own protocol support from the inside, and "the global
4//! is advertised" is a different claim from "a client that uses it gets the
5//! right answers back". The difference has been the whole story of a week of
6//! protocol work: a cursor that was advertised and invisible, a relative
7//! pointer that was advertised and correct, a lock that was granted too early
8//! to be heard.
9//!
10//! Real applications were borrowed for that verification — Firefox's own
11//! `WAYLAND_DEBUG` log is a fine oracle — and it works right up until no
12//! installed application happens to use the protocol you just added. Firefox
13//! never binds `wp_presentation`, so nothing on this machine could tell whether
14//! presentation feedback worked at all. This is that gap, filled.
15//!
16//!     cargo run -p wl-probe
17//!
18//! It maps one small window, waits for a few frames, and reports what came
19//! back. Exit status is zero only if every check passed, so it can be a gate.
20
21use 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
73/// How tall a probe bar is, and how much of the screen it reserves.
74const BAR: i32 = 48;
75
76/// How many frames to ask about before reporting.
77const FRAMES: usize = 5;
78
79#[derive(Default)]
80struct Probe {
81    /// Every global the compositor advertised, with its version.
82    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    /// Every `idled`/`resumed` in order, so a check can assert on the
92    /// *sequence* and not just the final state -- "it went idle, came back and
93    /// went idle again" is the claim, and only the sequence carries it.
94    idle_events: Vec<&'static str>,
95    seat: Option<WlSeat>,
96    /// The layouts named in the keymap the compositor sent, in order, and
97    /// which one is active. The only way a client can find out: the keymap
98    /// arrives as a file descriptor and nothing else describes it.
99    layouts: Vec<String>,
100    active_layout: Option<u32>,
101    repeat: Option<(i32, i32)>,
102    /// Where the compositor said the pointer was, in the surface's own
103    /// coordinates. The one number a client cannot check any other way: a
104    /// compositor that reports the wrong place has buttons that miss.
105    pointer_at: Vec<(f64, f64)>,
106    /// Whether the compositor said the session is locked, and whether it
107    /// refused. Exactly one of these is the answer to "did the lock take".
108    locked: bool,
109    lock_refused: bool,
110    /// Each lock surface the compositor configured, by output name and size.
111    lock_surfaces: Vec<(String, u32, u32)>,
112    decorations: Option<ZxdgDecorationManagerV1>,
113    presentation: Option<WpPresentation>,
114    /// What a capture told us to allocate, and how it went.
115    shot: Option<Shot>,
116    /// The bars, kept alive past the layer check.
117    ///
118    /// Not tidiness: the capture check looks for one of them in the top rows
119    /// of the image, which is the only way a client can tell an upside-down
120    /// screenshot from a right-way-up one. Dropped at the end of `main` with
121    /// everything else.
122    bars_alive: Vec<(WlSurface, ZwlrLayerSurfaceV1)>,
123    /// Every output, in the order advertised: the proxy, its name, and the
124    /// size of its current mode.
125    outputs: Vec<Screen>,
126    /// The size each layer surface was configured to, by the output it named.
127    bars: Vec<(String, u32, u32)>,
128    /// The clock id the compositor says its timestamps are on.
129    clock: Option<u32>,
130    surface: Option<WlSurface>,
131    configured: bool,
132    /// One entry per frame the compositor said it had presented.
133    presented: Vec<Presented>,
134    discarded: usize,
135}
136
137/// A screen capture in progress.
138#[derive(Debug, Default)]
139struct Shot {
140    /// From the `buffer` event: what to allocate.
141    format: Option<u32>,
142    width: u32,
143    height: u32,
144    stride: u32,
145    /// Whether the compositor finished describing the buffer.
146    described: bool,
147    /// Whether it said the copy is done, and whether it gave up.
148    ready: bool,
149    failed: bool,
150    /// The flags it reported, `Y_INVERT` among them.
151    flags: u32,
152}
153
154/// One `wl_output`, as a client learns about it.
155#[derive(Debug)]
156struct Screen {
157    output: WlOutput,
158    /// From `wl_output.name`, which is version 4 — the connector name, and the
159    /// same string `monitors` in the configuration uses.
160    name: String,
161    width: i32,
162    height: i32,
163    /// From `wl_output.scale`. Device pixels per logical one, as an integer —
164    /// the protocol has no fractional form, and a compositor rounds up.
165    scale: i32,
166}
167
168impl Screen {
169    /// The monitor's size in *logical* pixels, which is what every protocol
170    /// that positions anything speaks in.
171    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    /// Reassembled from the protocol's split seconds-hi/seconds-lo/nanos.
180    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    /// In group order, and a variant is simply part of the name -- which is
210    /// the whole reason this reads the group names rather than the section's.
211    #[test]
212    fn layouts_in_group_order() {
213        assert_eq!(
214            layouts_in_keymap_text(TWO),
215            ["Ukrainian", "English (Dvorak)"]
216        );
217    }
218
219    /// `xkb_types` is full of `level_name[1]= "Any"`. A looser match reports
220    /// the modifier levels as keyboard layouts, which is why the scan is
221    /// confined to the symbols section.
222    #[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    // Two roundtrips: the first brings the globals, the second whatever binding
252    // them produced -- the presentation clock arrives that way.
253    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    // Two windows to look at: one the compositor decorates, one that says it
261    // will decorate itself. Optionally one of them then asks for the screen.
262    //
263    // This exists because the things worth photographing here -- a
264    // server-side frame beside a client-side one, and what entering fullscreen
265    // looks like frame by frame -- need a client that will do exactly that on
266    // cue. Real applications will not: Firefox attaches to whatever instance
267    // is already running, and Electron takes half a minute to start and then
268    // argues about Vulkan.
269    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    // One window and a popup reaching past its bottom-right corner, in two
276    // colours nothing else draws, held up to be photographed: what a menu over
277    // a warped window looks like. `dev/present-check.sh`'s menu case.
278    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    // Watch for layout changes. Its own mode because it needs something else
285    // to do the switching, and because it deliberately sits still.
286    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    // Go idle, be held awake, go idle again. Not part of the ordinary run: it
299    // deliberately does nothing for several seconds, and a gate that sits
300    // still is a gate people stop running.
301    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    // Lock the session and hold it. Separate from the ordinary run rather than
312    // part of it, because a check that locks the screen in the middle of a
313    // gate is a check that locks the screen of whoever ran the gate.
314    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    // Connect, learn what is there, and leave -- with no surface of any kind.
326    //
327    // This exists to be a client lifecycle with no window in it. `super+return`
328    // in the soak both connects a client *and* maps a toplevel, so a leak that
329    // tracks it cannot be attributed to either; churning this instead can. See
330    // issue #33.
331    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            // Version 4 or nothing. A compositor that does not send a name
404            // leaves a client unable to say which monitor it wants, which is
405            // the whole of putting a bar on the right screen.
406            "(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    // A bar, a dock or a wallpaper is a layer surface, and which monitor one
425    // lands on cannot be checked from inside the compositor.
426    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    // A screenshot, which is the one check that looks at what the compositor
434    // actually drew rather than at what it said.
435    if probe.has("zwlr_screencopy_manager_v1") {
436        // The whole monitor, then a region of it — the two entry points the
437        // protocol has, and `grim` uses both.
438        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    // Presentation feedback needs a window on screen to be about.
447    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    // Held up so the compositor's own picture can be looked at: an exclusive
455    // zone is a claim about the *other* windows, and no client can see those.
456    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        // Where the pointer was told it was, while it was over a bar. The bars
469        // are anchored to the top of the screen at y 0, so with a click driven
470        // to a known place these coordinates should be that place. Printed
471        // rather than asserted because only the caller knows where it aimed.
472        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
497/// Map two windows -- one decorated by the compositor, one not -- and hold
498/// them up to be looked at.
499///
500/// `WL_PROBE_WINDOWS=<seconds>` holds them; `WL_PROBE_FULLSCREEN=1` makes the
501/// first one ask for the screen a second in, which is what makes the
502/// transition photographable.
503fn 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        // The half that matters: one of them tells the compositor it will
532        // decorate itself, which is what an Electron or GTK application does.
533        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    // Configured before anything is attached: a toplevel may not draw until it
547    // has been told a size once.
548    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    // Something with an obvious edge, so the frame around it is easy to see
558    // and a size change is easy to measure.
559    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        // Redraw whatever we were configured to, so a resize is honoured.
583        if queue.roundtrip(probe).is_err() {
584            break;
585        }
586        std::thread::sleep(Duration::from_millis(30));
587    }
588}
589
590/// `WL_PROBE_POPUP=<seconds>`: a 520x360 toplevel in the window colour and a
591/// 160x120 popup in the menu colour, anchored 20 px inside its bottom-right
592/// corner so it reaches past it.
593fn 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
660/// Ask for a screenshot, and look at it.
661///
662/// The only check here that examines what the compositor *drew* rather than
663/// what it said. Every other one reads an event; this one reads pixels, which
664/// is the difference between "the protocol answers" and "there is a desktop in
665/// the buffer".
666///
667/// Nothing that speaks this protocol is installed on the machine this was
668/// written on — no grim, no wf-recorder, no portal — so this is not a
669/// convenience. It is the only oracle there is.
670///
671/// `WL_PROBE_SHOT=<path>` writes the capture out as a binary PPM, because
672/// "some pixels were not zero" and "that is my desktop" are different claims
673/// and only one of them can be checked by a program.
674fn 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    // No cursor: a screenshot with somebody's mouse in it is the thing
692    // `overlay_cursor` exists to let you avoid, and asking for it here would
693    // make the pixel check depend on where the pointer happens to be.
694    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    // What the buffer *should* be: the whole monitor in device pixels, or the
732    // region asked for scaled the same way. A wrong size here is a capture of
733    // something other than what was asked for, and every check below would
734    // pass on it.
735    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    // A pool the size the compositor asked for, and a buffer over it.
750    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    // And now the part no event can tell us: is there a desktop in it?
789    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        // Reading back through /proc is a convenience, not a guarantee. Map
797        // the pool instead if it did not work.
798        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    // Not all zeros. An unbound framebuffer reads back as nothing at all, and
807    // that is the failure this catches -- but only that one: the compositor
808    // clears to a dark grey, so "not black" is true of an empty desktop too.
809    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    // More than one colour in it. A capture that is a single flat colour is
822    // either an empty clear or a copy of the wrong thing, and both look fine
823    // to the check above.
824    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    // And the right way up, or in the right place. A bar was anchored to the
845    // *top* of this monitor a moment ago, so its colour belongs in the top
846    // rows of a whole-output capture and in neither band of a capture that
847    // starts below it.
848    //
849    // The first of those is a check that cannot be done any other way: an
850    // upside-down screenshot is perfectly legible and reads as a compositor
851    // bug rather than a row-order one, which is exactly how it happened here.
852    // The second catches a region offset that was ignored, which would hand
853    // back the top-left corner whatever was asked for.
854    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                // The bar is a solid mid-blue; the desktop behind it is
863                // near-black and the windows a dark slate. Blue clearly
864                // dominant is the test, not an exact value — the buffer is
865                // BGRX, so blue is byte zero.
866                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        // A region that starts below the bar must not contain it. If it does,
884        // the offset was ignored and this is the top of the screen.
885        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
893/// The first output's width in device pixels.
894fn screen_pixels(probe: &Probe) -> i32 {
895    probe.outputs.first().map_or(0, |screen| screen.width)
896}
897
898/// Write a captured buffer out as a binary PPM, so a person can look at it.
899fn 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            // Xrgb8888 is little-endian BGRX in memory.
906            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
917/// Anchor a bar to the top of every monitor and check where each one landed.
918///
919/// This is the dock question, and it is one a compositor cannot answer about
920/// itself. A layer surface names the output it wants; if the compositor puts it
921/// on a different monitor, the configure comes back with the wrong width, and
922/// that is measurable from out here and from nowhere else.
923///
924/// The exclusive zone is the other half, and it is *not* checkable from a
925/// client: it is a claim about where the compositor puts everybody else's
926/// windows. `WL_PROBE_HOLD=<seconds>` keeps the bars up so the compositor's own
927/// frame can be captured and looked at.
928fn 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    // One monitor, when asked for. A bar on every screen cannot tell "each
947    // monitor's own work area" from "every monitor's work area" — the picture
948    // looks the same either way. One bar on one screen can.
949    let only = std::env::var("WL_PROBE_BAR").ok();
950    // The *logical* width, because that is what a layer surface is configured
951    // in. Comparing against the mode said a bar on a 2x monitor had landed on
952    // another monitor, which is what this check is for and was wrong about:
953    // the compositor was right and the expectation was 1x-only.
954    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        // Named explicitly. A surface that names no output is put wherever the
982        // compositor's primary monitor is, which is the right default and the
983        // wrong thing to test with: it cannot tell a correct answer from a
984        // coincidence.
985        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        // The claim the rest of the desktop has to honour: this many pixels
996        // off *this* monitor's work area, and no other monitor's.
997        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    // Configured, so they can be drawn. Something visible, because the point
1013    // of holding them up is to look at them.
1014    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                    // Anchored left and right, so the width it is given is the
1034                    // width of the monitor it is on. A different number means a
1035                    // different monitor.
1036                    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    // Held past this function: see `Probe::bars_alive`.
1052    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
1062/// Put a window up, ask about `FRAMES` frames, and check the answers.
1063fn 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        // 1 is CLOCK_MONOTONIC. Anything else is legal but means every
1081        // timestamp below is on a clock this program is not reading.
1082        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    // Wait to be configured before attaching anything: a client may not attach
1095    // a buffer until it has been told its size once.
1096    //
1097    // `roundtrip` and not `blocking_dispatch`. The latter waits for an event
1098    // that may never come, and a probe that hangs is worse than a probe that
1099    // reports nothing -- it takes the shell it was run from with it. A
1100    // roundtrip sends a sync and waits for its reply, so it returns as long as
1101    // the compositor is alive at all.
1102    for turn in 0..50 {
1103        // Only if it is taking unusually long. A compositor that configures
1104        // promptly should say nothing; one that never does should say what it
1105        // is waiting for rather than looking hung.
1106        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        // Ask about *this* frame before committing it. The callback is
1122        // per-commit, which is the whole point: it answers about one frame.
1123        presentation.feedback(&surface, &handle, ());
1124        surface.attach(Some(&buffer), 0, 0);
1125        surface.damage(0, 0, i32::MAX, i32::MAX);
1126        surface.commit();
1127        // Long enough for a compositor that only draws on damage to have drawn
1128        // and, on real hardware, for the page flip to have happened.
1129        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    // Timestamps must go forward. Out of order means the compositor is
1157    // reporting something other than when the frame was shown.
1158    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    // A monotonic timestamp near zero means it is being measured from this
1167    // process rather than from boot, which is the mistake that looks fine
1168    // locally and breaks anything comparing against its own clock.
1169    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
1183/// Flush, wait for the compositor to answer, dispatch whatever arrived.
1184///
1185/// Bounded by construction: the sync it sends is answered by any live
1186/// compositor, so this cannot wait forever on a compositor that has simply
1187/// stopped having anything to say.
1188fn 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
1201/// A buffer with something in it, so the compositor has a frame to present.
1202fn solid_buffer(
1203    shm: &wl_shm::WlShm,
1204    handle: &QueueHandle<Probe>,
1205    width: i32,
1206    height: i32,
1207) -> WlBuffer {
1208    // Opaque mid-blue, premultiplied — anything non-zero, so the compositor is
1209    // presenting a real frame rather than nothing.
1210    tinted_buffer(shm, handle, width, height, [0x80, 0x50, 0x20, 0xff])
1211}
1212
1213/// The same, in a colour of the caller's choosing.
1214///
1215/// Worth the parameter because the whole point of some of these checks is
1216/// telling whose pixels are on screen: the lock screen has to be a colour
1217/// nothing else in the compositor draws, or a screenshot proves nothing.
1218fn 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
1240/// An anonymous file of `size` bytes, filled with an opaque colour.
1241fn 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    // `O_TMPFILE` would be tidier; a named file in the runtime dir that is
1253    // unlinked immediately is portable and does the same thing.
1254    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
1267/// Map a window, take focus, and report every layout change that arrives.
1268///
1269/// `WL_PROBE_KEYBOARD=<seconds>`. The compositor switching its own active
1270/// layout is one claim and the client being *told* is a different one, and
1271/// only the second matters -- a layout that changed and was not announced is a
1272/// keyboard that types the wrong letters. A client only hears about it through
1273/// `wl_keyboard.modifiers`, and only while it has focus, so this maps a window
1274/// first and holds it.
1275fn 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
1342/// Go idle, be held awake, and go idle again.
1343///
1344/// Run with `WL_PROBE_IDLE=1`. The two protocols this exercises are only
1345/// correct together, so it checks them together, as one sequence:
1346///
1347///   1. ask to be told after a short timeout, touch nothing, and be told;
1348///   2. take an inhibitor on a mapped window, and be told we are back;
1349///   3. hold it through more than the timeout, and be told nothing;
1350///   4. drop it, and be told again.
1351///
1352/// Steps 2 and 4 are the ones that matter. A compositor that advertises the
1353/// inhibitor and ignores it passes step 1 and step 3 looks identical to a
1354/// compositor whose timer simply stopped -- so the check is the whole
1355/// sequence, not any one event in it.
1356fn 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    // Short, because this runs in a gate. Long enough that one slow frame
1376    // cannot make the compositor look late.
1377    let timeout_ms: u32 = 700;
1378    let timeout = Duration::from_millis(u64::from(timeout_ms));
1379
1380    println!();
1381    // A window first: an inhibitor applies while its surface is *visible*, so
1382    // a check that takes one on an unmapped surface is asking the compositor
1383    // to be wrong.
1384    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    // 1. Nobody is touching anything, so this should arrive.
1421    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    // 2. The veto. A player asks to stay awake, and the answer should be
1433    //    immediate -- inhibition is not "start the timer again", it is "you
1434    //    are not idle".
1435    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    // 3. And it holds. This is the step that would catch an inhibitor the
1446    //    compositor accepted and then forgot about one frame later.
1447    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    // 4. And releasing it puts things back, rather than leaving the seat awake
1461    //    for good.
1462    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    // 5. And an inhibitor stops counting behind a lock screen. A player left
1473    //    running would otherwise keep the machine awake all night showing a
1474    //    lock screen, which is what neither feature is for. Opt-in, because it
1475    //    locks the screen of whoever ran it.
1476    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        // The inhibitor is still held, and deliberately not touched. The
1494        // window behind the lock screen has not gone anywhere; it has stopped
1495        // being visible, and that is the whole claim.
1496        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    // 6. And a window the compositor has moved off every screen stops holding
1512    //    the machine awake, which is what a workspace switch does to the
1513    //    windows it hides. The compositor has to do the moving, so this waits
1514    //    while something else -- `SOLIUM_TRIGGER_AT` with a workspace binding
1515    //    -- does it. See dev/README.md for the pair of commands.
1516    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
1550/// Lock the session, hold it, and let it go.
1551///
1552/// Run with `WL_PROBE_LOCK=<seconds>`. This is the only lock client on the
1553/// machine, and without one the protocol cannot be checked at all: the
1554/// compositor cannot lock itself, and every question worth asking about a lock
1555/// screen -- does the desktop stop being visible, do bindings stop firing,
1556/// does a monitor with no surface go blank rather than clear -- is a question
1557/// about what a *second* process can see.
1558///
1559/// It deliberately does not draw a password field. What it draws is a flat
1560/// colour, chosen to be nothing like the compositor's own backdrop, so a
1561/// screenshot answers "whose pixels are these" without anyone having to guess.
1562fn 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    // One surface per monitor, and on purpose one *fewer* than that when asked.
1592    // A lock screen that covers the monitor it was told about and leaves the
1593    // other showing the desktop is the failure this protocol exists to
1594    // prevent, and it is invisible unless something deliberately declines to
1595    // cover a screen. `WL_PROBE_LOCK_SKIP=<name>` is that.
1596    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        // No commit yet. A lock surface may not attach a buffer before its
1609        // first configure, and the size to attach is in that configure.
1610        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        // Bright green, and nothing else in this compositor is bright green.
1640        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    // The only legitimate way out. Destroying the lock object *without* this
1667    // must leave the session locked, which is the other half of the protocol
1668    // and is checked by `WL_PROBE_LOCK_ABANDON=1` below.
1669    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            // Not an error to receive: it is how a compositor says another
1695            // client already holds the lock. It *is* an error to ignore --
1696            // after `finished` the session is not locked and never will be by
1697            // this client.
1698            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            // Version 4 for `wl_output.name`, which is the connector name and
1751            // the only way a client can say *which* monitor it means.
1752            "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        // A ping unanswered is a client the compositor is entitled to kill.
1853        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            // The keymap is a file descriptor holding XKB text, and it is the
1922            // only place a client can learn what layouts it has. No event
1923            // names them, so the file has to be read and parsed -- which is
1924            // also the only way to check a compositor's keyboard
1925            // configuration from the outside.
1926            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
1936/// Read the keymap the compositor sent and pull the layout names out of it.
1937///
1938/// XKB text names its layouts in the `xkb_symbols` section, as a `+`-joined
1939/// recipe like `pc+us+ua:2+inet(evdev)`. Parsing that is unlovely and it is
1940/// what the information is: there is no protocol event that says "you have
1941/// these two layouts", so every client that shows a layout indicator does
1942/// this or does without.
1943fn layouts_in_keymap(fd: std::os::fd::OwnedFd, size: usize) -> Vec<String> {
1944    // `read_at`, not `read`. The compositor's own file offset is wherever it
1945    // left it -- at the end, having just written the keymap -- and the fd
1946    // carries that offset with it, so a plain `read` returns zero bytes and
1947    // looks exactly like a compositor that sent nothing. The protocol tells
1948    // clients to mmap this, which is why every real toolkit works and this
1949    // did not; `read_at` is the same thing without a dependency.
1950    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    // `WL_PROBE_KEYMAP=<path>` writes it out. A keymap is the compositor's
1962    // most opaque answer -- tens of thousands of lines behind a file
1963    // descriptor -- and "the layouts look right" is a different claim from
1964    // "here is what it sent".
1965    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
1973/// The layout names a keymap declares, in group order.
1974///
1975/// From the `name[N]="..."` lines inside the compiled `xkb_symbols` section,
1976/// which is the one place in a keymap that names layouts unambiguously and in
1977/// the language a person reads: `"Ukrainian"`, `"English (Dvorak)"`.
1978///
1979/// Two wrong turns are worth recording, because both looked right.
1980///
1981/// The section's own name is a recipe -- `pc_ua_us_2_inet(evdev)_group(alt_shift_toggle)`
1982/// -- and parsing it is a trap: the separator is `_`, the options' arguments
1983/// contain `_` themselves, the group index arrives as a bare token because
1984/// `us:2` is written `us_2`, and a layout with a variant (`us(dvorak)`) cannot
1985/// be told from an option (`inet(evdev)`) without a list of every option group
1986/// xkb has, which would rot.
1987///
1988/// The second was `name[Group1]=`, which is what `xkbcomp` writes and what
1989/// every example online shows. libxkbcommon writes `name[1]=`. The difference
1990/// is invisible until you read a keymap this compositor actually sent, which
1991/// is what `WL_PROBE_KEYMAP` is for.
1992///
1993/// Confined to the symbols section on purpose: `xkb_types` is full of
1994/// `level_name[1]= "Any"`, and a looser match reports the modifier levels as
1995/// keyboard layouts.
1996fn 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        // `enter` and `motion` both carry the position *within the surface*,
2038        // and that is the only number here worth recording: the compositor
2039        // works in its own coordinates and has to subtract the surface's
2040        // corner to get this one. Subtracting the wrong corner is invisible
2041        // from inside the compositor and is a bar whose buttons all miss.
2042        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                // Version 3 sends `buffer_done`; older versions do not, so a
2098                // buffer event is enough on its own to start allocating.
2099                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            // The *current* mode, not every mode the monitor offers -- an
2129            // output sends one of these per mode it supports.
2130            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                // Acknowledged before anything is attached: the protocol says
2161                // a surface may not draw until it has agreed a size, and a
2162                // compositor is within its rights to kill a client that does.
2163                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}