diff options
| author | srdusr <[email protected]> | 2025-06-16 23:40:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-06-16 23:40:00 +0200 |
| commit | 5f26a48fbb772ccc9c9ffb1209ff00d3c11d88c7 (patch) | |
| tree | 48ff3f548d20eb529d10f18d8f356aeba64a1aec /tools/toplevel-activate/src | |
| parent | 3f2ac4d85c4e66c2f1ae6622bf3c2f302abe93b9 (diff) | |
| download | srdwm-5f26a48fbb772ccc9c9ffb1209ff00d3c11d88c7.tar.gz srdwm-5f26a48fbb772ccc9c9ffb1209ff00d3c11d88c7.zip | |
Add a minimal zwlr_foreign_toplevel activate test tool; confirm aegis's focus-staleness report no longer reproduces
tools/toplevel-activate: a standalone (not a workspace member - its own
empty [workspace] table, so building srdwm itself never has to build
this too) wayland-client + wayland-protocols-wlr binary that lists every
open zwlr_foreign_toplevel_handle_v1, activates one by index, and prints
the resulting `activated` state from the protocol's own feedback.
wayland-client 0.31.14 / wayland-protocols-wlr 0.3.12 - the exact
versions smithay 0.7.0 already pulls in, so this talks to the same real
client library srdwm itself is built against, not a possibly-drifted one.
Used it to reproduce aegis's own exact repro (nested `srdwm --wayland`,
two plain alacritty windows, activate the non-focused one, check
`srd clients`) precisely: launched a real nested instance, activated
back and forth 5 times, checked `srd clients` immediately and after a
delay each time. Every check matched the protocol's own `activated`
feedback - no staleness found, on the nested/winit backend specifically
(the peer's own repro environment). Documented in docs/TODO.md as
likely already fixed by other focus/window-management work since the
original report, not re-root-caused after the fact, but confirmed not
currently reproducible via the exact repro that found it - left open
one more round in case it resurfaces, with this tool as the fastest way
back to a live repro if it does.
Diffstat (limited to 'tools/toplevel-activate/src')
| -rw-r--r-- | tools/toplevel-activate/src/main.rs | 135 |
1 files changed, 135 insertions, 0 deletions
diff --git a/tools/toplevel-activate/src/main.rs b/tools/toplevel-activate/src/main.rs new file mode 100644 index 0000000..beddacd --- /dev/null +++ b/tools/toplevel-activate/src/main.rs @@ -0,0 +1,135 @@ +// Minimal zwlr_foreign_toplevel_manager_v1 client: lists every open +// toplevel, optionally activates one by index, then exits. Built to +// reproduce srdwm's own aegis-reported bug (srd clients' `focused` field +// going stale after an activate-driven focus change) with a real protocol +// client instead of guessing from source reading alone. +// +// Usage: +// activate -- list toplevels with their index +// activate <index> -- activate the toplevel at that index, then list again + +use std::collections::HashMap; + +use wayland_client::protocol::{wl_registry, wl_seat::WlSeat}; +use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle}; +use wayland_protocols_wlr::foreign_toplevel::v1::client::{ + zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1}, + zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, +}; + +#[derive(Default, Debug, Clone)] +struct ToplevelInfo { + title: String, + app_id: String, + activated: bool, +} + +struct State { + seat: Option<WlSeat>, + manager: Option<ZwlrForeignToplevelManagerV1>, + toplevels: HashMap<u32, (ZwlrForeignToplevelHandleV1, ToplevelInfo)>, + next_index: u32, +} + +impl Dispatch<wl_registry::WlRegistry, ()> for State { + fn event(state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, qh: &QueueHandle<Self>) { + if let wl_registry::Event::Global { name, interface, version } = event { + if interface == "wl_seat" { + state.seat = Some(registry.bind::<WlSeat, _, _>(name, version.min(9), qh, ())); + } else if interface == "zwlr_foreign_toplevel_manager_v1" { + state.manager = Some(registry.bind::<ZwlrForeignToplevelManagerV1, _, _>(name, version.min(3), qh, ())); + } + } + } +} + +impl Dispatch<WlSeat, ()> for State { + fn event(_: &mut Self, _: &WlSeat, _: wayland_client::protocol::wl_seat::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {} +} + +impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for State { + fn event_created_child(opcode: u16, qh: &QueueHandle<Self>) -> std::sync::Arc<dyn wayland_client::backend::ObjectData> { + // Opcode 0 is the `toplevel` event, whose one argument is a new + // `zwlr_foreign_toplevel_handle_v1` object - wayland-client needs + // to know the object-data for that new id *before* the event + // itself is dispatched, which is what this override provides. + match opcode { + 0 => qh.make_data::<ZwlrForeignToplevelHandleV1, ()>(()), + _ => panic!("unexpected new-object event opcode {opcode} on zwlr_foreign_toplevel_manager_v1"), + } + } + + fn event(state: &mut Self, _: &ZwlrForeignToplevelManagerV1, event: zwlr_foreign_toplevel_manager_v1::Event, _: &(), _: &Connection, _qh: &QueueHandle<Self>) { + if let zwlr_foreign_toplevel_manager_v1::Event::Toplevel { toplevel } = event { + let idx = state.next_index; + state.next_index += 1; + // `toplevel` (the new handle) already has its object data set + // by `event_created_child` above by the time this event fires + // - events for it just get matched back to our own map by + // proxy equality in the handle `Dispatch` impl below, rather + // than needing the object data itself to carry the index. + state.toplevels.insert(idx, (toplevel, ToplevelInfo::default())); + } + } +} + +impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for State { + fn event(state: &mut Self, proxy: &ZwlrForeignToplevelHandleV1, event: zwlr_foreign_toplevel_handle_v1::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) { + let Some((_, info)) = state.toplevels.values_mut().find(|(h, _)| *h == *proxy) else { return }; + match event { + zwlr_foreign_toplevel_handle_v1::Event::Title { title } => info.title = title, + zwlr_foreign_toplevel_handle_v1::Event::AppId { app_id } => info.app_id = app_id, + zwlr_foreign_toplevel_handle_v1::Event::State { state: bytes } => { + // Each state entry is a native-endian u32; 2 == Activated per + // the protocol's own enum (Maximized=0, Minimized=1, + // Activated=2, Fullscreen=3). + info.activated = bytes.chunks_exact(4).any(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]]) == 2); + } + _ => {} + } + } +} + +fn main() { + let conn = Connection::connect_to_env().expect("failed to connect to the Wayland compositor - is WAYLAND_DISPLAY set?"); + let display = conn.display(); + let mut event_queue: EventQueue<State> = conn.new_event_queue(); + let qh = event_queue.handle(); + let _registry = display.get_registry(&qh, ()); + + let mut state = State { seat: None, manager: None, toplevels: HashMap::new(), next_index: 0 }; + event_queue.roundtrip(&mut state).expect("initial roundtrip failed"); + // A second roundtrip: title/app_id/state/done events for each toplevel + // arrive in a batch right after the manager announces it, not + // necessarily inside the same roundtrip that discovered the manager. + event_queue.roundtrip(&mut state).expect("second roundtrip failed"); + + if state.manager.is_none() { + eprintln!("compositor does not advertise zwlr_foreign_toplevel_manager_v1"); + std::process::exit(1); + } + let Some(seat) = state.seat.clone() else { + eprintln!("compositor does not advertise wl_seat"); + std::process::exit(1); + }; + + let arg = std::env::args().nth(1); + if let Some(idx_str) = arg { + let idx: u32 = idx_str.parse().expect("argument must be a toplevel index (see the no-argument listing)"); + let Some((handle, info)) = state.toplevels.get(&idx) else { + eprintln!("no toplevel with index {idx}"); + std::process::exit(1); + }; + println!("activating [{idx}] app_id={:?} title={:?}", info.app_id, info.title); + handle.activate(&seat); + event_queue.roundtrip(&mut state).expect("activate roundtrip failed"); + println!("activate() call sent and flushed."); + } + + println!("-- toplevels --"); + let mut entries: Vec<_> = state.toplevels.iter().collect(); + entries.sort_by_key(|(idx, _)| **idx); + for (idx, (_, info)) in entries { + println!("[{idx}] app_id={:?} title={:?} activated={}", info.app_id, info.title, info.activated); + } +} |