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 | |
| 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')
| -rw-r--r-- | tools/toplevel-activate/Cargo.lock | 216 | ||||
| -rw-r--r-- | tools/toplevel-activate/Cargo.toml | 17 | ||||
| -rw-r--r-- | tools/toplevel-activate/src/main.rs | 135 |
3 files changed, 368 insertions, 0 deletions
diff --git a/tools/toplevel-activate/Cargo.lock b/tools/toplevel-activate/Cargo.lock new file mode 100644 index 0000000..870f73e --- /dev/null +++ b/tools/toplevel-activate/Cargo.lock @@ -0,0 +1,216 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "toplevel-activate-test" +version = "0.1.0" +dependencies = [ + "wayland-client", + "wayland-protocols-wlr", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/tools/toplevel-activate/Cargo.toml b/tools/toplevel-activate/Cargo.toml new file mode 100644 index 0000000..bbfe6fc --- /dev/null +++ b/tools/toplevel-activate/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "toplevel-activate-test" +version = "0.1.0" +edition = "2021" + +# Standalone - deliberately not a member of the main workspace at +# ../../Cargo.toml, so building/testing srdwm itself never has to build +# this too. A debug tool, not part of the shipped product. +[workspace] + +[[bin]] +name = "activate" +path = "src/main.rs" + +[dependencies] +wayland-client = "=0.31.14" +wayland-protocols-wlr = { version = "=0.3.12", features = ["client"] } 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); + } +} |