diff options
| author | srdusr <[email protected]> | 2024-07-31 16:22:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-07-31 16:22:00 +0200 |
| commit | c180bfc34e4c74ba7cac9e0220e9c172c239fe71 (patch) | |
| tree | 45550c7eb7ae819a9c3a415acb9cfa5be1cfd03b /crates/x11/src/platform/connect.rs | |
| parent | 38683f069a039f9da6d7ddc64167850560fd07ed (diff) | |
| download | srdwm-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.rs | 193 |
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(()) + } +} |