summaryrefslogtreecommitdiff
path: root/src/conn_info
diff options
context:
space:
mode:
authortcmal <me@aria.rip>2024-06-21 19:13:12 +0100
committertcmal <me@aria.rip>2024-06-21 19:13:12 +0100
commit475253b7bcfd03a932c4b7efd969b3d2bf155035 (patch)
tree44789ce271d14c89cbff777e75f1e9ab6e1a5e64 /src/conn_info
parentc3e98e34ed7d42ef4271339de88f7131e7647442 (diff)
refactor connection-related stuff out, make things a bit cleaner
Diffstat (limited to 'src/conn_info')
-rw-r--r--src/conn_info/atoms.rs18
-rw-r--r--src/conn_info/colours.rs50
-rw-r--r--src/conn_info/cursors.rs59
-rw-r--r--src/conn_info/keys.rs104
-rw-r--r--src/conn_info/mod.rs225
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!()
+ }
+}