diff options
author | tcmal <me@aria.rip> | 2024-06-21 19:13:12 +0100 |
---|---|---|
committer | tcmal <me@aria.rip> | 2024-06-21 19:13:12 +0100 |
commit | 475253b7bcfd03a932c4b7efd969b3d2bf155035 (patch) | |
tree | 44789ce271d14c89cbff777e75f1e9ab6e1a5e64 /src/conn_info | |
parent | c3e98e34ed7d42ef4271339de88f7131e7647442 (diff) |
refactor connection-related stuff out, make things a bit cleaner
Diffstat (limited to 'src/conn_info')
-rw-r--r-- | src/conn_info/atoms.rs | 18 | ||||
-rw-r--r-- | src/conn_info/colours.rs | 50 | ||||
-rw-r--r-- | src/conn_info/cursors.rs | 59 | ||||
-rw-r--r-- | src/conn_info/keys.rs | 104 | ||||
-rw-r--r-- | src/conn_info/mod.rs | 225 |
5 files changed, 456 insertions, 0 deletions
diff --git a/src/conn_info/atoms.rs b/src/conn_info/atoms.rs new file mode 100644 index 0000000..2d23c81 --- /dev/null +++ b/src/conn_info/atoms.rs @@ -0,0 +1,18 @@ +xcb::atoms_struct! { + #[derive(Copy, Clone, Debug)] + pub struct Atoms { + pub wm_protocols => b"WM_PROTOCOLS", + pub wm_delete => b"WM_DELETE_WINDOW", + pub wm_state => b"WM_STATE", + pub wm_take_focus => b"WM_TAKE_FOCUS", + pub net_active_window => b"_NET_ACTIVE_WINDOW", + pub net_supported => b"_NET_SUPPORTED", + pub net_wm_name => b"_NET_WM_NAME", + pub net_wm_state => b"_NET_WM_STATE", + pub net_wm_check => b"_NET_SUPPORTING_WM_CHECK", + pub net_wm_fullscreen => b"_NET_WM_STATE_FULLSCREEN", + pub net_wm_window_type => b"_NET_WM_WINDOW_TYPE", + pub net_wm_window_type_dialog => b"_NET_WM_WINDOW_TYPE_DIALOG", + pub net_client_list => b"_NET_CLIENT_LIST", + } +} diff --git a/src/conn_info/colours.rs b/src/conn_info/colours.rs new file mode 100644 index 0000000..22b4fc2 --- /dev/null +++ b/src/conn_info/colours.rs @@ -0,0 +1,50 @@ +use crate::error::Result; +use xcb::{ + x::{AllocColor, Colormap}, + Connection, +}; + +/// Caches colours in an X11 color map. +pub struct Colours { + #[allow(unused)] // Make sure the colour map we're using doesn't go anywhere + cmap: Colormap, + border_normal: u32, + border_focused: u32, +} + +impl Colours { + /// Load the colours into the given colour map + pub fn new_with(conn: &Connection, cmap: Colormap) -> Result<Self> { + // TODO: Move these colours out to config.rs + let (border_normal, border_focused) = ( + conn.wait_for_reply(conn.send_request(&AllocColor { + cmap, + red: 0, + green: 0, + blue: 0, + }))?, + conn.wait_for_reply(conn.send_request(&AllocColor { + cmap, + red: u16::MAX, + green: 0, + blue: u16::MAX, + }))?, + ); + + Ok(Self { + cmap, + border_normal: border_normal.pixel(), + border_focused: border_focused.pixel(), + }) + } + + /// Get the pixel ID of the colour for an unfocused window's border. + pub const fn border_normal(&self) -> u32 { + self.border_normal + } + + /// Get the pixel ID of the colour for a focused window's border. + pub const fn border_focused(&self) -> u32 { + self.border_focused + } +} diff --git a/src/conn_info/cursors.rs b/src/conn_info/cursors.rs new file mode 100644 index 0000000..119de5c --- /dev/null +++ b/src/conn_info/cursors.rs @@ -0,0 +1,59 @@ +use crate::error::Result; +use xcb::{ + x::{CreateGlyphCursor, Cursor, Font, OpenFont}, + Connection, +}; + +// https://tronche.com/gui/x/xlib/appendix/b/ +const XC_LEFT_PTR: u16 = 68; + +/// Caches X11 cursor objects +#[derive(Debug)] +pub struct Cursors { + #[allow(unused)] // Needs to be kept around since the cursors depend on it + font: Font, + + normal: Cursor, +} + +impl Cursors { + /// Load default cursors using the given connection. + pub fn new_with(conn: &Connection) -> Result<Self> { + // Open cursor font + let font = conn.generate_id(); + conn.check_request(conn.send_request_checked(&OpenFont { + fid: font, + name: b"cursor", + }))?; + + Ok(Self { + normal: Self::load_cursor(conn, font, XC_LEFT_PTR)?, + font, + }) + } + + /// Load the cursor with the given id from `font` + fn load_cursor(conn: &Connection, font: Font, id: u16) -> Result<Cursor> { + let cid = conn.generate_id(); + // https://github.com/mirror/libX11/blob/ff8706a5eae25b8bafce300527079f68a201d27f/src/Cursor.c#L34 + conn.check_request(conn.send_request_checked(&CreateGlyphCursor { + cid, + source_font: font, + mask_font: font, + source_char: id, + mask_char: id + 1, + fore_red: 0, + fore_green: 0, + fore_blue: 0, + back_red: u16::MAX, + back_green: u16::MAX, + back_blue: u16::MAX, + }))?; + + Ok(cid) + } + + pub const fn normal(&self) -> Cursor { + self.normal + } +} diff --git a/src/conn_info/keys.rs b/src/conn_info/keys.rs new file mode 100644 index 0000000..1be3dfe --- /dev/null +++ b/src/conn_info/keys.rs @@ -0,0 +1,104 @@ +use std::ops::RangeInclusive; + +use crate::Result; +use xcb::{ + x::{GetKeyboardMapping, GetKeyboardMappingReply, GetModifierMapping, ModMask}, + Connection, +}; +use xkeysym::{KeyCode, Keysym, RawKeyCode}; + +/// Cached information about our keyboard layout. +pub struct KeyboardInfo { + /// The range of keycodes used + keycodes: RangeInclusive<RawKeyCode>, + + /// The `ModMask` corresponding to `NumLock`. + /// This varies sometimes, and we need to know to ignore it. + numlock_mask: ModMask, + + /// The mapping from keycodes to (multiple) key symbols + mapping: GetKeyboardMappingReply, +} + +impl KeyboardInfo { + /// Query information about the keyboard layout from the given connection. + pub fn new_with(conn: &Connection) -> Result<Self> { + let min_keycode = conn.get_setup().min_keycode(); + let max_keycode = conn.get_setup().max_keycode(); + + let mapping = conn.wait_for_reply(conn.send_request(&GetKeyboardMapping { + first_keycode: min_keycode, + count: max_keycode - min_keycode + 1, + }))?; + + let mut this = Self { + keycodes: RawKeyCode::from(min_keycode)..=RawKeyCode::from(max_keycode), + numlock_mask: ModMask::empty(), + mapping, + }; + + let Some(numlock_keycode) = this.keysym_to_keycode(Keysym::Num_Lock) else { + // No numlock button, so no modmask for numlock + return Ok(this); + }; + let mod_map = conn.wait_for_reply(conn.send_request(&GetModifierMapping {}))?; + let keypermod = mod_map.keycodes().len() / 8; + for i in 0..8 { + for j in 0..keypermod { + if RawKeyCode::from(mod_map.keycodes()[i * keypermod + j]) == numlock_keycode.raw() + { + this.numlock_mask = + ModMask::from_bits(1 << i).expect("x11 returned unrecognised modifier"); + } + } + } + + Ok(this) + } + + /// Get the modifier mask being used for numlock + pub const fn numlock_mask(&self) -> ModMask { + self.numlock_mask + } + + /// Iterate over all keycodes and their bound keysyms. + /// This is likely to contain duplicate pairs. + pub fn iter_keycodes_keysyms(&self) -> impl Iterator<Item = (KeyCode, Keysym)> + '_ { + (0..self.mapping.keysyms_per_keycode()) + .flat_map(|shift| self.keycodes.clone().map(move |keycode| (shift, keycode))) + .filter_map(|(shift, keycode)| -> Option<_> { + Some(( + keycode.into(), + self.keycode_to_keysym(keycode.into(), shift)?, + )) + }) + } + + /// Lookup the first keycode which has the given keysym in any column + pub(crate) fn keysym_to_keycode(&self, keysym: Keysym) -> Option<KeyCode> { + for shift in 0..self.mapping.keysyms_per_keycode() { + for keycode in self.keycodes.clone() { + if self.mapping.keysyms()[(keycode as usize - *self.keycodes.start() as usize) + * self.mapping.keysyms_per_keycode() as usize + + shift as usize] + == keysym.raw() + { + return Some(keycode.into()); + } + } + } + + None + } + + /// Lookup the keysym in the given column for the given keycode + pub fn keycode_to_keysym(&self, keycode: KeyCode, col: u8) -> Option<Keysym> { + xkeysym::keysym( + keycode, + col, + (*self.keycodes.start()).into(), + self.mapping.keysyms_per_keycode(), + self.mapping.keysyms(), + ) + } +} diff --git a/src/conn_info/mod.rs b/src/conn_info/mod.rs new file mode 100644 index 0000000..623e2a5 --- /dev/null +++ b/src/conn_info/mod.rs @@ -0,0 +1,225 @@ +use xcb::{ + x::{ + self, ChangeProperty, ChangeWindowAttributes, CreateWindow, DeleteProperty, DestroyWindow, + Window, WindowClass, + }, + Connection as RawConnection, +}; + +mod atoms; +mod colours; +mod cursors; +mod keys; + +pub use self::{atoms::Atoms, colours::Colours, cursors::Cursors, keys::KeyboardInfo}; +use crate::error::{Error, Result}; + +/// The connection, along with some cached resources required for WM operations. +pub struct Connection<'a> { + /// The open connection to an X server + conn: &'a RawConnection, + + /// The 'screen' number on the X server. Note this isn’t what you think it is on multi-monitor setups + screen_num: usize, + + /// The root window + root: Window, + + /// A window used to prove we're actually EWMH compliant. + /// See [the EWMH spec](https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html#idm46187912047344) + // TODO: Destroy this properly + check_window: Window, + + /// Cached colours, + pub colours: Colours, + + /// Cached cursors + pub cursors: Cursors, + + /// Cached atoms + pub atoms: Atoms, + + /// Cached keyboard layout information + pub keyboard_state: KeyboardInfo, +} + +impl<'a> Connection<'a> { + /// Prepare the window manager to run on the given connection and screen number. + /// This will fail if another WM is running. + pub fn new(conn: &'a RawConnection, screen_num: usize) -> Result<Self> { + // Fetch root window + let setup = conn.get_setup(); + let screen = setup.roots().nth(screen_num).ok_or(Error::NoSuchScreen)?; + + // Check no other WM is running + conn.check_request(conn.send_request_checked(&ChangeWindowAttributes { + window: screen.root(), + value_list: &[ + x::Cw::BackPixel(screen.white_pixel()), + x::Cw::EventMask(x::EventMask::SUBSTRUCTURE_REDIRECT), + ], + })) + .map_err(|_| Error::OtherWMRunning)?; + + // Create check window + let check_window = conn.generate_id(); + let root = screen.root(); + let atoms = Atoms::intern_all(conn)?; + conn.send_request(&CreateWindow { + wid: check_window, + parent: root, + depth: 0, + x: 0, + y: 0, + width: 0, + height: 0, + border_width: 0, + class: WindowClass::InputOutput, + visual: 0, + value_list: &[], + }); + + conn.send_request(&ChangeProperty { + mode: x::PropMode::Replace, + window: root, + property: atoms.net_wm_check, + r#type: x::ATOM_WINDOW, + data: &[check_window], + }); + + conn.send_request(&ChangeProperty { + mode: x::PropMode::Replace, + window: check_window, + property: atoms.net_wm_check, + r#type: x::ATOM_WINDOW, + data: &[check_window], + }); + + conn.send_request(&ChangeProperty { + mode: x::PropMode::Replace, + window: check_window, + property: atoms.net_wm_name, + r#type: x::ATOM_STRING, + data: b"blow", + }); + + // Supported flag + conn.send_request(&ChangeProperty { + mode: x::PropMode::Replace, + window: root, + property: atoms.net_supported, + r#type: x::ATOM_ATOM, + data: &[ + atoms.net_active_window, + atoms.net_wm_name, + atoms.net_wm_state, + atoms.net_wm_check, + atoms.net_wm_fullscreen, + atoms.net_wm_window_type, + atoms.net_wm_window_type_dialog, + atoms.net_client_list, + ], + }); + + // Cleanup state + conn.send_request(&DeleteProperty { + window: root, + property: atoms.net_client_list, + }); + + // Get the right events + let cursors = Cursors::new_with(conn)?; + conn.send_request(&ChangeWindowAttributes { + window: root, + value_list: &[ + x::Cw::EventMask( + x::EventMask::SUBSTRUCTURE_REDIRECT + | x::EventMask::SUBSTRUCTURE_NOTIFY + | x::EventMask::BUTTON_PRESS + | x::EventMask::ENTER_WINDOW + | x::EventMask::FOCUS_CHANGE + | x::EventMask::STRUCTURE_NOTIFY + | x::EventMask::PROPERTY_CHANGE, + ), + x::Cw::Cursor(cursors.normal()), + ], + }); + + Ok(Self { + colours: Colours::new_with(conn, screen.default_colormap())?, + atoms: Atoms::intern_all(conn)?, + cursors: Cursors::new_with(conn)?, + keyboard_state: KeyboardInfo::new_with(conn)?, + check_window: conn.generate_id(), + conn, + screen_num, + root: screen.root(), + }) + } + + /// Get the root window our WM is using + pub const fn root(&self) -> Window { + self.root + } + + /// Refresh cached info about keyboard layout + pub fn refresh_keyboard_info(&mut self) -> Result<()> { + self.keyboard_state = KeyboardInfo::new_with(self.conn)?; + Ok(()) + } + + /// Delegate for [`RawConnection::send_request`] + pub fn send_request<R>(&self, req: &R) -> R::Cookie + where + R: xcb::Request, + { + self.conn.send_request(req) + } + + /// Delegate for [`RawConnection::send_and_check_request`] + pub fn send_and_check_request<R>(&self, req: &R) -> Result<()> + where + R: xcb::RequestWithoutReply, + { + self.conn.send_and_check_request(req).map_err(Into::into) + } + + /// Delegate for [`RawConnection::flush`] + pub fn flush(&self) -> Result<()> { + self.conn.flush().map_err(Into::into) + } + + /// Delegate for [`RawConnection::wait_for_event`] + pub fn wait_for_event(&self) -> Result<xcb::Event> { + self.conn.wait_for_event().map_err(Into::into) + } + + pub fn active_extensions(&self) -> impl Iterator<Item = xcb::Extension> + '_ { + self.conn.active_extensions() + } + + pub fn wait_for_reply<C>(&self, cookie: C) -> xcb::Result<C::Reply> + where + C: xcb::CookieWithReplyChecked, + { + self.conn.wait_for_reply(cookie) + } + + pub fn get_setup(&self) -> &x::Setup { + self.conn.get_setup() + } + + pub const fn screen_num(&self) -> usize { + self.screen_num + } +} + +impl Drop for Connection<'_> { + fn drop(&mut self) { + self.send_request(&DestroyWindow { + window: self.check_window, + }); + // TODO: Unset attributes of root window + todo!() + } +} |