srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/x11/src/platform/connect.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-07-31 16:22:00 +0200
committersrdusr <[email protected]>2024-07-31 16:22:00 +0200
commitc180bfc34e4c74ba7cac9e0220e9c172c239fe71 (patch)
tree45550c7eb7ae819a9c3a415acb9cfa5be1cfd03b /crates/x11/src/platform/connect.rs
parent38683f069a039f9da6d7ddc64167850560fd07ed (diff)
downloadsrdwm-c180bfc34e4c74ba7cac9e0220e9c172c239fe71.tar.gz
srdwm-c180bfc34e4c74ba7cac9e0220e9c172c239fe71.zip
Split crates/x11/src/lib.rs (925 lines) into platform/
Pure reorganization, no behavior change - verified by diffing the function-name and struct/trait-name sets before/after (both identical) plus a full cargo test pass. lib.rs is now a thin shim (mod declaration + pub use), same pattern crates/config used, since a crate root can't itself become a directory. platform/mod.rs keeps the atom table, Frame/X11Platform's struct definitions, the small free-function helpers (err, modmask_for_keycode_in_mod_slots, rgb_to_pixel), and the ClonedForRender trait+impl. The rest splits by concern: - connect.rs: connect, keymap/modifier helpers, grab_keybindings. - window.rs: manage_new_window and the other per-client lifecycle methods (window_title/class, supports_wm_delete, unmanage, frame_for). - events.rs: handle_event, the X11 event-dispatch loop. - actions.rs: raise_and_focus/request_close/sync_geometry/ redraw_all_decorations. - trait_impl.rs: `impl Platform for X11Platform` - named to avoid clippy's module_inception lint, since the containing directory is already named `platform`. - tests.rs: unsplit, same reasoning as every other split this pass. A handful of X11Platform methods (frame_for, manage_new_window, unmanage, raise_and_focus, request_close, sync_geometry, keycode_to_keysym, modifiers_from_state, handle_event) went from private to pub(super): called across what are now sibling submodules, which Rust's privacy model doesn't let see each other's private items.
Diffstat (limited to 'crates/x11/src/platform/connect.rs')
-rw-r--r--crates/x11/src/platform/connect.rs193
1 files changed, 193 insertions, 0 deletions
diff --git a/crates/x11/src/platform/connect.rs b/crates/x11/src/platform/connect.rs
new file mode 100644
index 0000000..d5c3a59
--- /dev/null
+++ b/crates/x11/src/platform/connect.rs
@@ -0,0 +1,193 @@
+use super::*;
+impl X11Platform {
+ pub fn connect(wm: Rc<RefCell<WindowManager>>) -> PlatformResult<Self> {
+ let (conn, screen_num) = RustConnection::connect(None).map_err(|e| PlatformError::ConnectionFailed(e.to_string()))?;
+ let root = conn.setup().roots[screen_num].root;
+
+ // Registering for SUBSTRUCTURE_REDIRECT is how X tells us "no other
+ // WM may do this" - if one already has it, this request comes back
+ // as a checked BadAccess. This replaces the legacy check, which
+ // always returned true because its error handler discarded errors.
+ let aux = ChangeWindowAttributesAux::new()
+ .event_mask(EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE);
+ conn.change_window_attributes(root, &aux).map_err(err)?.check().map_err(|_| PlatformError::AnotherWmRunning)?;
+
+ let atoms = Atoms::new(&conn).map_err(err)?.reply().map_err(err)?;
+ conn.change_property32(x11rb::protocol::xproto::PropMode::REPLACE, root, atoms._NET_SUPPORTED, x11rb::protocol::xproto::AtomEnum::ATOM, &[
+ atoms._NET_WM_STATE,
+ atoms._NET_WM_STATE_MAXIMIZED_VERT,
+ atoms._NET_WM_STATE_MAXIMIZED_HORZ,
+ atoms._NET_CLIENT_LIST,
+ atoms._NET_ACTIVE_WINDOW,
+ ]).map_err(err)?;
+
+ let font = conn.generate_id().map_err(err)?;
+ conn.open_font(font, b"fixed").map_err(err)?;
+
+ let gc = conn.generate_id().map_err(err)?;
+ let gc_aux = CreateGCAux::new().font(font).graphics_exposures(0);
+ conn.create_gc(gc, root, &gc_aux).map_err(err)?;
+
+ let setup = conn.setup();
+ let (min_keycode, max_keycode) = (setup.min_keycode, setup.max_keycode);
+ let mapping = conn
+ .get_keyboard_mapping(min_keycode, max_keycode - min_keycode + 1)
+ .map_err(err)?
+ .reply()
+ .map_err(err)?;
+ let keysyms_per_keycode = mapping.keysyms_per_keycode;
+ let keyboard_mapping = mapping.keysyms;
+
+ // Num Lock's modifier bit is not fixed by the X11 spec (unlike Caps
+ // Lock, which is always `ModMask::LOCK`) - it's whichever of
+ // Mod1..Mod5 the server happens to have bound it to, keyboard- and
+ // OS-dependent. Found the same way every other X11 WM does: look up
+ // Num Lock's keycode (keysym `0xff7f`, XK_Num_Lock) in the keyboard
+ // mapping just queried above, then find which modifier slot's
+ // keycode list contains it. See `grab_keybindings`'s doc comment
+ // for why this is needed at all.
+ let numlock_mask = {
+ const XK_NUM_LOCK: u32 = 0xff7f;
+ let numlock_keycode = (min_keycode..=max_keycode).find(|&kc| {
+ let idx = (kc - min_keycode) as usize * keysyms_per_keycode as usize;
+ keyboard_mapping.get(idx).copied() == Some(XK_NUM_LOCK)
+ });
+ match numlock_keycode {
+ Some(kc) => {
+ let modmap = conn.get_modifier_mapping().map_err(err)?.reply().map_err(err)?;
+ let per = modmap.keycodes_per_modifier() as usize;
+ modmask_for_keycode_in_mod_slots(kc, per, &modmap.keycodes)
+ }
+ None => ModMask::from(0u16),
+ }
+ };
+
+ conn.flush().map_err(err)?;
+
+ // Same socket name convention as the Wayland backends
+ // (`srdwm-<display>.sock`) - there, `<display>` is the Wayland
+ // socket's own name; here, the only display identity X11 has is
+ // `$DISPLAY` itself (e.g. `:0`), which is exactly what every X
+ // client - including a nested Xephyr/Xnest session used for
+ // testing - already keys off to tell one server from another.
+ let display_name = std::env::var("DISPLAY").unwrap_or_else(|_| "x11".to_string());
+ let ipc = match srdwm_platform::IpcServer::bind(&display_name) {
+ Ok(ipc) => Some(ipc),
+ Err(e) => {
+ log::warn!("failed to bind srd IPC socket for display '{display_name}': {e}");
+ None
+ }
+ };
+
+ Ok(Self {
+ conn,
+ root,
+ atoms,
+ gc,
+ font,
+ wm,
+ xid_to_core: HashMap::new(),
+ frames: HashMap::new(),
+ min_keycode,
+ max_keycode,
+ keysyms_per_keycode,
+ keyboard_mapping,
+ numlock_mask,
+ ipc,
+ })
+ }
+
+ pub(super) fn keycode_to_keysym(&self, keycode: u8) -> u32 {
+ if keycode < self.min_keycode || keycode > self.max_keycode || self.keysyms_per_keycode == 0 {
+ return 0;
+ }
+ let idx = (keycode - self.min_keycode) as usize * self.keysyms_per_keycode as usize;
+ self.keyboard_mapping.get(idx).copied().unwrap_or(0)
+ }
+
+ fn keysym_to_keycode(&self, keysym: u32) -> Option<u8> {
+ for kc in self.min_keycode..=self.max_keycode {
+ let idx = (kc - self.min_keycode) as usize * self.keysyms_per_keycode as usize;
+ if self.keyboard_mapping.get(idx).copied() == Some(keysym) {
+ return Some(kc);
+ }
+ }
+ None
+ }
+
+ pub(super) fn modifiers_from_state(state: u16) -> Modifiers {
+ let mut m = Modifiers::empty();
+ if state & ModMask::SHIFT.bits() != 0 {
+ m |= Modifiers::SHIFT;
+ }
+ if state & ModMask::CONTROL.bits() != 0 {
+ m |= Modifiers::CTRL;
+ }
+ if state & ModMask::M1.bits() != 0 {
+ m |= Modifiers::ALT;
+ }
+ if state & ModMask::M4.bits() != 0 {
+ m |= Modifiers::SUPER;
+ }
+ m
+ }
+
+ fn modmask_for(modifiers: Modifiers) -> ModMask {
+ let mut mask = ModMask::from(0u16);
+ if modifiers.contains(Modifiers::SHIFT) {
+ mask |= ModMask::SHIFT;
+ }
+ if modifiers.contains(Modifiers::CTRL) {
+ mask |= ModMask::CONTROL;
+ }
+ if modifiers.contains(Modifiers::ALT) {
+ mask |= ModMask::M1;
+ }
+ if modifiers.contains(Modifiers::SUPER) {
+ mask |= ModMask::M4;
+ }
+ mask
+ }
+
+ /// Grabs the given `"Mod4+Shift+Return"`-style key combos on the root
+ /// window so their KeyPress events reach us even when a client has
+ /// input focus. Call after loading config (once bindings are known).
+ ///
+ /// A `KeyPress`'s modifier state includes whichever lock modifiers
+ /// happen to be toggled on (Num Lock, Caps Lock) in addition to
+ /// whatever the binding actually asked for - `XGrabKey` matches state
+ /// *exactly*, not as a subset, so a grab registered only for e.g.
+ /// `Mod4` never fires the moment Num Lock is on, since the real event's
+ /// state is `Mod4 | numlock_mask` instead. Every real X11 WM (i3,
+ /// bspwm, dwm) grabs each binding once per combination of the lock
+ /// modifiers for exactly this reason; this one previously didn't,
+ /// which meant every keybinding silently stopped firing the instant
+ /// Num Lock was toggled on - not a missing feature, a basic X11
+ /// correctness requirement that was simply never implemented.
+ pub fn grab_keybindings(&mut self, combos: &[String]) -> PlatformResult<()> {
+ // The four combinations of "Num Lock toggled or not" x "Caps Lock
+ // toggled or not" - Scroll Lock is deliberately not covered here,
+ // matching the convention every WM referenced above also follows
+ // (rarely present on modern keyboards, rarely toggled when it is).
+ let lock_variants = [ModMask::from(0u16), self.numlock_mask, ModMask::LOCK, self.numlock_mask | ModMask::LOCK];
+ for combo in combos {
+ let Some((modifiers, key_name)) = srdwm_core::parse_key_combo(combo) else { continue };
+ let Some(keysym) = keysyms::name_to_keysym(key_name) else {
+ log::warn!("cannot grab '{combo}': unknown key name '{key_name}'");
+ continue;
+ };
+ let Some(keycode) = self.keysym_to_keycode(keysym) else {
+ log::warn!("cannot grab '{combo}': no keycode for keysym {keysym:#x}");
+ continue;
+ };
+ let mask = Self::modmask_for(modifiers);
+ for lock in lock_variants {
+ self.conn
+ .grab_key(true, self.root, mask | lock, keycode, GrabMode::ASYNC, GrabMode::ASYNC)
+ .map_err(err)?;
+ }
+ }
+ self.conn.flush().map_err(err)?;
+ Ok(())
+ }
+}