srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-06-16 23:40:00 +0200
committersrdusr <[email protected]>2025-06-16 23:40:00 +0200
commit5f26a48fbb772ccc9c9ffb1209ff00d3c11d88c7 (patch)
tree48ff3f548d20eb529d10f18d8f356aeba64a1aec /tools
parent3f2ac4d85c4e66c2f1ae6622bf3c2f302abe93b9 (diff)
downloadsrdwm-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.lock216
-rw-r--r--tools/toplevel-activate/Cargo.toml17
-rw-r--r--tools/toplevel-activate/src/main.rs135
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);
+ }
+}