From 475253b7bcfd03a932c4b7efd969b3d2bf155035 Mon Sep 17 00:00:00 2001 From: tcmal Date: Fri, 21 Jun 2024 19:13:12 +0100 Subject: refactor connection-related stuff out, make things a bit cleaner --- src/atoms.rs | 18 -- src/clients.rs | 710 ----------------------------------------------- src/clients/client.rs | 174 ++++++++++++ src/clients/hints.rs | 104 +++++++ src/clients/mod.rs | 425 ++++++++++++++++++++++++++++ src/clients/monitors.rs | 55 ++++ src/clients/tile.rs | 68 +++++ src/colours.rs | 50 ---- src/conn_info/atoms.rs | 18 ++ src/conn_info/colours.rs | 50 ++++ src/conn_info/cursors.rs | 59 ++++ src/conn_info/keys.rs | 104 +++++++ src/conn_info/mod.rs | 225 +++++++++++++++ src/cursors.rs | 59 ---- src/error.rs | 10 +- src/focus.rs | 75 +---- src/keys.rs | 198 ++++--------- src/main.rs | 244 +++------------- 18 files changed, 1390 insertions(+), 1256 deletions(-) delete mode 100644 src/atoms.rs delete mode 100644 src/clients.rs create mode 100644 src/clients/client.rs create mode 100644 src/clients/hints.rs create mode 100644 src/clients/mod.rs create mode 100644 src/clients/monitors.rs create mode 100644 src/clients/tile.rs delete mode 100644 src/colours.rs create mode 100644 src/conn_info/atoms.rs create mode 100644 src/conn_info/colours.rs create mode 100644 src/conn_info/cursors.rs create mode 100644 src/conn_info/keys.rs create mode 100644 src/conn_info/mod.rs delete mode 100644 src/cursors.rs diff --git a/src/atoms.rs b/src/atoms.rs deleted file mode 100644 index 2d23c81..0000000 --- a/src/atoms.rs +++ /dev/null @@ -1,18 +0,0 @@ -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/clients.rs b/src/clients.rs deleted file mode 100644 index e9efeab..0000000 --- a/src/clients.rs +++ /dev/null @@ -1,710 +0,0 @@ -use std::cmp::min; - -use crate::{config::BORDER_WIDTH, error::*, WM}; -use xcb::{ - x::{ - self, ChangeProperty, ChangeWindowAttributes, ConfigWindow, ConfigureNotifyEvent, - ConfigureRequestEvent, ConfigureWindow, Cw, DestroyNotifyEvent, Drawable, EventMask, - GetGeometry, GetProperty, GetWindowAttributes, MapRequestEvent, MapWindow, Pixmap, - SendEvent, SendEventDest, UnmapNotifyEvent, UnmapWindow, Window, - }, - xinerama::{self, ScreenInfo}, - BaseEvent, Connection, Extension, Xid, XidNew, -}; - -impl WM<'_> { - /// Update the client recorded monitors and monitor sizes - /// Returns true if any values changed, meaning windows should be re-tiled. - pub(crate) fn update_geometry(&mut self) -> Result { - let mut dirty = false; - if self - .conn - .active_extensions() - .any(|e| e == Extension::Xinerama) - { - let reply = self - .conn - .wait_for_reply(self.conn.send_request(&xinerama::QueryScreens {}))?; - - // Monitor removed, move its clients away - if reply.screen_info().len() > self.clients.monitor_count() { - dirty = true; - self.clients.truncate_screens(reply.screen_info().len()); - } - - // Update screen info & add new client lists if needed - for (i, monitor) in reply.screen_info().iter().enumerate() { - dirty |= self.clients.set_monitor_geometry(i, (*monitor).into()); - } - } else { - // Only one screen - if self.clients.monitor_count() > 1 { - dirty = true; - self.clients.truncate_screens(1); - } - - // TODO: it looks like this won't actually update when the screen size changes? - let setup = self.conn.get_setup(); - let screen = setup - .roots() - .nth(self.screen_num as usize) - .ok_or(Error::NoSuchScreen)?; - - dirty |= self.clients.set_monitor_geometry( - 0, - MonitorGeometry { - x_org: 0, - y_org: 0, - width: screen.width_in_pixels(), - height: screen.height_in_pixels(), - }, - ); - } - - Ok(dirty) - } - - /// Update our monitor geometry if the root window is reconfigured - pub(crate) fn handle_configure_notify(&mut self, e: ConfigureNotifyEvent) -> Result<()> { - if e.window() == self.root && self.update_geometry()? { - self.clients.rearrange(self.conn); - self.conn.flush()?; - } - Ok(()) - } - - /// Perform configure requests if we're happy with them, or they're for an unmanaged window. - pub(crate) fn handle_configure_request(&mut self, e: ConfigureRequestEvent) -> Result<()> { - if let Some(c) = self.clients.find_client_mut(e.window()) { - // TODO: Allow changing some properties: - // - Border width - // - Size and position if floating - c.configure_notify(self.conn); - self.conn.flush()?; - } else { - // Configure it as requested, and sort the rest when we actually map the window - self.conn - .check_request(self.conn.send_request_checked(&ConfigureWindow { - window: e.window(), - value_list: &[ - ConfigWindow::X(e.x().into()), - ConfigWindow::Y(e.y().into()), - ConfigWindow::Width(e.width().into()), - ConfigWindow::Height(e.height().into()), - ConfigWindow::BorderWidth(e.border_width().into()), - ConfigWindow::StackMode(e.stack_mode()), - ], - }))? - } - - Ok(()) - } - - /// Removing destroyed windows from the client list and rearrange. - pub(crate) fn handle_destroy_notify(&mut self, e: DestroyNotifyEvent) -> Result<()> { - if self.clients.remove_client(e.window()).is_some() { - self.clients.rearrange(self.conn); - self.conn.flush()?; - } - - Ok(()) - } - - /// Map a window, starting to manage it if needed. - pub(crate) fn handle_map_request(&mut self, e: MapRequestEvent) -> Result<()> { - // Ignore already managed windows - if self.clients.find_client_mut(e.window()).is_some() { - return Ok(()); - } - - let attrs = self.conn.wait_for_reply( - self.conn - .send_request(&GetWindowAttributes { window: e.window() }), - )?; - if attrs.override_redirect() { - // Something special, don't manage it just let it do its thing. - return Ok(()); - } - - // Start managing, and map window - self.manage(self.conn, e.window()); - - self.conn.flush()?; - - Ok(()) - } - - /// When a window is unmapped, either stop managing it or update its state. - pub(crate) fn handle_unmap_notify(&mut self, e: UnmapNotifyEvent) -> Result<()> { - if self.clients.find_client_mut(e.window()).is_some() { - if e.is_from_send_event() { - self.conn.send_request(&ChangeProperty { - mode: xcb::x::PropMode::Replace, - window: e.window(), - property: self.atoms.wm_state, - r#type: self.atoms.wm_state, - data: &[0_u8, 0_u8], - }); - } else { - self.clients.remove_client(e.window()); - self.clients.rearrange(self.conn); - // TODO: 'disown' the window - unmanage(c, 0) - } - } - - Ok(()) - } - - /// Start managing the given window, adding it to the client list and ensuring its configuration is valid. - /// This function sends multiple requests without checking them, so conn.flush() should be called after. - fn manage(&mut self, conn: &Connection, window: Window) { - // TODO: inherit from parent if window is transient - let mon = self.clients.focused_mon(); - - let Ok(geom) = conn.wait_for_reply(conn.send_request(&GetGeometry { - drawable: Drawable::Window(window), - })) else { - return; // window stopped existing, so we can't manage it - }; - - self.unfocus(); - - // TODO: inserting at index 0 is why dwm uses linked lists, maybe this can be improved - self.clients.mons[mon].clients.insert( - 0, - Client { - window, - x: 0, - y: 0, - width: 0, - height: 0, - border_width: BORDER_WIDTH, - mapped: false, - urgent: false, - never_focus: false, - }, - ); - - // TODO: Clamp window size to monitor - - let c = &mut self.clients.mons[mon].clients[0]; - - c.set_geom( - conn, - geom.x(), - geom.y(), - geom.width(), - geom.height(), - BORDER_WIDTH, - ); - c.set_border(conn, self.colours.border_normal()); - c.ensure_mapped(conn); - - // TODO: updatewindowtype - // TODO: updatesizehints - // TODO: updatewmhints - c.sync_properties(conn, true); - - // XSelectInput(dpy, w, EnterWindowMask|FocusChangeMask|PropertyChangeMask|StructureNotifyMask); - c.set_event_mask( - conn, - EventMask::ENTER_WINDOW - | EventMask::FOCUS_CHANGE - | EventMask::PROPERTY_CHANGE - | EventMask::STRUCTURE_NOTIFY, - ); - // TODO: grabbuttons - - // set ewmh - self.conn.send_request(&ChangeProperty { - mode: xcb::x::PropMode::Append, - window: self.root, - property: self.atoms.net_client_list, - r#type: x::ATOM_WINDOW, - data: &[window], - }); - self.set_client_withdrawn(window, false); - - // TODO: XMoveResizeWindow(dpy, c->win, c->x + 2 * sw, c->y, c->w, c->h); /* some windows require this */ - self.refocus(mon, 0); - self.clients.rearrange_monitor(conn, mon); - } - - /// Set the given window as withdrawn / not withdrawn. - fn set_client_withdrawn(&self, window: Window, withdrawn: bool) { - self.conn.send_request(&ChangeProperty { - mode: xcb::x::PropMode::Replace, - window, - property: self.atoms.wm_state, - r#type: self.atoms.wm_state, - data: &[!withdrawn as u8, 0_u8], - }); - } -} - -/// Holds state related to the window manager's clients -/// This contains a list of clients per monitor, alongside info on that monitor's screen size. -pub struct ClientState { - /// The current arranging function. - /// This function is expected to ensure that all clients are the correct size, reconfigure them if needed, and map/unmap as needed. - /// The connection will be flushed after it is called. - arrange: &'static dyn Fn(&mut MonitorInfo, &Connection), - - /// A client list for each monitor. - mons: Vec, - - /// Co-ordinates to the currently focused window. - focused: (usize, usize), -} - -/// Info stored for each monitor -#[derive(Debug)] -pub struct MonitorInfo { - /// Clients attached to that monitor - clients: Vec, - - /// The monitor's geometry - screen_info: MonitorGeometry, -} - -impl MonitorInfo { - /// Iterate over all tiled clients, returning a mutable reference to each. - pub fn clients_tiled_mut(&mut self) -> impl Iterator { - // TODO: tag filtering, floating - self.clients.iter_mut() - } -} - -impl ClientState { - /// Get the amount of monitors this state is currently aware of - pub fn monitor_count(&self) -> usize { - self.mons.len() - } - - /// Set the new amount of screens, without unmanaging any clients. - pub fn truncate_screens(&mut self, new_size: usize) { - // hack: working around double borrow stuff - let mut moved_clients = vec![]; - for old in self.mons.drain(new_size - self.mons.len()..self.mons.len()) { - moved_clients.extend(old.clients.into_iter()); - } - self.mons[0].clients.extend(moved_clients); - } - - /// Set the given screen's geometry, resizing the monitor list if necessary. - /// Returns true if the new info is different from the old one. - pub fn set_monitor_geometry(&mut self, i: usize, info: MonitorGeometry) -> bool { - while i >= self.mons.len() { - self.mons.push(MonitorInfo::default()) - } - let dirty = self.mons[i].screen_info != info; - self.mons[i].screen_info = info; - - dirty - } - - /// Find the [`Client`] corresponding to the given window - pub fn find_client_mut(&mut self, window: Window) -> Option<&mut Client> { - let (mon, i) = self.find_client_pos(window)?; - Some(&mut self.mons[mon].clients[i]) - } - - /// Find the position of the client with the given window, returning (monitor, index) - pub fn find_client_pos(&mut self, window: Window) -> Option<(usize, usize)> { - for (pos_mon, mon) in self.mons.iter_mut().enumerate() { - if let Some(pos) = mon.clients.iter().position(|c| c.window == window) { - return Some((pos_mon, pos)); - } - } - - None - } - - /// Get the position of the currently focused client. This position may be invalid. - pub fn focused(&self) -> (usize, usize) { - self.focused - } - - /// Get a mutable reference to the currently focused client, if it exists. - pub fn focused_mut(&mut self) -> Option<&mut Client> { - self.client_mut(self.focused.0, self.focused.1) - } - - /// Set the currently focused client, returning a mutable reference to it if the co-ordinates are valid. - pub fn set_focused(&mut self, mut mon: usize, mut i: usize) -> Option<&mut Client> { - if self.mons.is_empty() { - return None; - } - - if mon >= self.mons.len() { - mon = self.mons.len() - 1; - } - - if self.mons[mon].clients.is_empty() { - return None; - } - - if i >= self.mons[mon].clients.len() { - i = self.mons[mon].clients.len() - 1; - } - - self.focused = (mon, i); - Some(&mut self.mons[mon].clients[i]) - } - - /// Get a mutable reference to the client at the given co-ordinates, if they are valid. - pub fn client_mut(&mut self, mon: usize, i: usize) -> Option<&mut Client> { - if mon < self.mons.len() && i < self.mons[mon].clients.len() { - Some(&mut self.mons[mon].clients[i]) - } else { - None - } - } - - /// Remove the client associated with the given window. - /// This doesn't perform any of the associated X11 stuff - pub fn remove_client(&mut self, window: Window) -> Option { - let (mon, i) = self.find_client_pos(window)?; - Some(self.mons[mon].clients.remove(i)) - } - - /// Rearrange all clients, reconfiguring them as needed. - /// This function sends multiple requests without checking them, so conn.flush() should be called after. - pub fn rearrange(&mut self, conn: &Connection) { - for mon in 0..self.monitor_count() { - self.rearrange_monitor(conn, mon); - } - } - - /// Rearrange a specific monitor - /// This function sends multiple requests without checking them, so conn.flush() should be called after. - fn rearrange_monitor(&mut self, conn: &Connection, mon: usize) { - (self.arrange)(&mut self.mons[mon], conn); - } - - /// Get the currently focused monitor - fn focused_mon(&self) -> usize { - self.focused.0 - } -} - -/// Information about a single client / window -#[derive(Debug)] -pub struct Client { - /// The corresponding X11 window - window: Window, - - x: i16, - y: i16, - width: u16, - height: u16, - border_width: u16, - mapped: bool, - - urgent: bool, - never_focus: bool, -} - -impl Client { - /// Send a configure configure notify event with the current geometry. - /// This function does not check for success, so conn.flush() should be called after. - fn configure_notify(&self, conn: &Connection) { - conn.send_request(&SendEvent { - destination: SendEventDest::Window(self.window), - event_mask: EventMask::STRUCTURE_NOTIFY, - event: &ConfigureNotifyEvent::new( - self.window, - self.window, - Window::none(), - self.x, - self.y, - self.width, - self.height, - self.border_width, - false, - ), - propagate: false, - }); - } - - /// Set this client's geometry, also updating the X11 window if needed. - /// This function does not check for success, so conn.flush() should be called after. - fn set_geom( - &mut self, - conn: &Connection, - x: i16, - y: i16, - width: u16, - height: u16, - border_width: u16, - ) { - if (x, y, width, height, border_width) - == (self.x, self.y, self.width, self.height, self.border_width) - { - return; - } - - self.x = x; - self.y = y; - self.width = width; - self.height = height; - self.border_width = border_width; - - conn.send_request(&ConfigureWindow { - window: self.window, - value_list: &[ - ConfigWindow::X(self.x.into()), - ConfigWindow::Y(self.y.into()), - ConfigWindow::Width(self.width.into()), - ConfigWindow::Height(self.height.into()), - ConfigWindow::BorderWidth(self.border_width.into()), - ], - }); - } - - /// Set the border colour of the X11 window to the given value (see `crate::colours::Colours`) - /// This function does not check for success, so conn.flush() should be called after. - pub fn set_border(&self, conn: &Connection, colour: u32) { - conn.send_request(&ChangeWindowAttributes { - window: self.window(), - value_list: &[Cw::BorderPixel(colour)], - }); - } - - /// Ensure this client is currently mapped / visible - /// This function does not check for success, so conn.flush() should be called after. - pub fn ensure_mapped(&mut self, conn: &Connection) { - if !self.mapped { - conn.send_request(&MapWindow { - window: self.window, - }); - self.mapped = true; - } - } - - /// Ensure this client is currently unmapped / invisible - /// This function does not check for success, so conn.flush() should be called after. - pub fn ensure_unmapped(&mut self, conn: &Connection) { - if self.mapped { - conn.send_request(&UnmapWindow { - window: self.window, - }); - self.mapped = false; - } - } - - /// Get the associated window - pub fn window(&self) -> Window { - self.window - } - - /// Set the event mask for this window - /// This function does not check for success, so conn.flush() should be called after. - fn set_event_mask(&self, conn: &Connection, event_mask: EventMask) { - conn.send_request(&ChangeWindowAttributes { - window: self.window(), - value_list: &[Cw::EventMask(event_mask)], - }); - } - - /// Sync the non-geometry related properties with EWMH hints - /// This function does not check for success, so conn.flush() should be called after. - pub fn sync_properties(&mut self, conn: &Connection, focused: bool) { - let Some(mut hints) = XWMHints::get(conn, self.window) else { - return; - }; - - if focused && hints.is_urgent() { - hints.set_urgent(false); - hints.set(conn, self.window); - } else { - self.urgent = hints.is_urgent(); - } - - self.never_focus = if hints.input_valid() { - !hints.input - } else { - false - }; - } -} - -struct XWMHints { - flags: u32, - input: bool, - initial_state: i32, - icon_pixmap: Pixmap, - icon_window: Window, - icon_x: i32, - icon_y: i32, - icon_mask: Pixmap, - window_group: u32, -} - -impl XWMHints { - pub fn get(conn: &Connection, window: Window) -> Option { - // https://github.com/mirror/libX11/blob/ff8706a5eae25b8bafce300527079f68a201d27f/src/GetHints.c#L106 - // https://github.com/mirror/libX11/blob/master/src/Xatomtype.h#L111 - let hints = conn - .wait_for_reply(conn.send_request(&GetProperty { - window: window, - delete: false, - property: x::ATOM_WM_HINTS, - r#type: x::ATOM_WM_HINTS, - long_offset: 0, - long_length: 9, - })) - .ok()?; - - if hints.r#type() != x::ATOM_WM_HINTS || hints.length() < 8 || hints.format() != 32 { - return None; - } - - let [flags, input, initial_state, icon_pixmap, icon_window, icon_x, icon_y, icon_mask, window_group] = - match hints.value::() { - [f, i, is, ip, iw, ix, iy, im, wg] => [f, i, is, ip, iw, ix, iy, im, wg], - [f, i, is, ip, iw, ix, iy, im] => [f, i, is, ip, iw, ix, iy, im, &0], - _ => unreachable!(), - }; - - unsafe { - Some(Self { - flags: *flags, - input: *input > 0, - initial_state: std::mem::transmute::(*initial_state), - icon_pixmap: Pixmap::new(*icon_pixmap), - icon_window: Window::new(*icon_window), - icon_x: std::mem::transmute::(*icon_x), - icon_y: std::mem::transmute::(*icon_y), - icon_mask: Pixmap::new(*icon_mask), - window_group: *window_group, - }) - } - } - - pub fn set(&self, conn: &Connection, window: Window) { - todo!() - } - - pub fn is_urgent(&self) -> bool { - (self.flags & (1 << 8)) > 0 - } - - pub fn set_urgent(&mut self, urgent: bool) { - self.flags &= (1 << 8) - } - - fn input_valid(&self) -> bool { - (self.flags & (1 << 0)) > 0 - } -} - -impl Default for MonitorInfo { - fn default() -> Self { - Self { - clients: vec![], - screen_info: MonitorGeometry { - x_org: 0, - y_org: 0, - width: 0, - height: 0, - }, - } - } -} - -/// A simple tiling function -fn tile(mon: &mut MonitorInfo, conn: &Connection) { - if mon.clients.is_empty() { - return; - } - - let n = mon.clients_tiled_mut().count(); - let nmaster = 1; - let mfact = 0.6; - - let MonitorGeometry { - x_org, - y_org, - width: mon_width, - height: mon_height, - } = mon.screen_info; - - let main_width = if nmaster == 0 { - 0 - } else if n > nmaster { - ((mon.screen_info.width as f64) * mfact) as u16 - } else { - mon.screen_info.width - }; - - let (mut main_y, mut second_y) = (0, 0); - for (i, c) in mon.clients_tiled_mut().enumerate() { - if i < nmaster { - let h = (mon_height - main_y) / (min(nmaster, n) - i) as u16; - c.set_geom( - conn, - x_org, - y_org + main_y as i16, - main_width - (2 * c.border_width), - h - (2 * c.border_width), - c.border_width, - ); - - main_y += h; - } else { - let h = (mon_height - second_y) / (n - i) as u16; - c.set_geom( - conn, - x_org + main_width as i16, - y_org + second_y as i16, - mon_width - main_width - (2 * c.border_width), - h - (2 * c.border_width), - c.border_width, - ); - - second_y += h; - } - - c.ensure_mapped(conn); - } -} - -impl Default for ClientState { - fn default() -> Self { - Self { - arrange: &tile, - focused: (0, 0), - mons: vec![], - } - } -} - -impl std::fmt::Debug for ClientState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ClientState") - .field("focused", &self.focused) - .field("mons", &self.mons) - .finish() - } -} - -/// Info on the monitor's geometry. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MonitorGeometry { - pub x_org: i16, - pub y_org: i16, - pub width: u16, - pub height: u16, -} - -impl From for MonitorGeometry { - fn from(value: ScreenInfo) -> Self { - Self { - x_org: value.x_org, - y_org: value.y_org, - width: value.width, - height: value.height, - } - } -} diff --git a/src/clients/client.rs b/src/clients/client.rs new file mode 100644 index 0000000..a7b1a8d --- /dev/null +++ b/src/clients/client.rs @@ -0,0 +1,174 @@ +use xcb::{ + x::{ + ChangeProperty, ChangeWindowAttributes, ConfigWindow, ConfigureNotifyEvent, + ConfigureWindow, Cw, EventMask, MapWindow, SendEvent, SendEventDest, Window, + }, + Xid, +}; + +use crate::{config::BORDER_WIDTH, conn_info::Connection}; + +use super::hints; + +/// Information about a single client / window +#[derive(Debug)] +pub struct Client { + /// The corresponding X11 window + window: Window, + + x: i16, + y: i16, + width: u16, + height: u16, + border_width: u16, + mapped: bool, + + urgent: bool, + never_focus: bool, +} + +impl Client { + pub const fn new(window: Window) -> Self { + Self { + window, + x: 0, + y: 0, + width: 0, + height: 0, + border_width: BORDER_WIDTH, + mapped: false, + urgent: false, + never_focus: false, + } + } + + /// Send a configure configure notify event with the current geometry. + /// This function does not check for success, so `conn.flush()` should be called after. + pub fn configure_notify(&self, conn: &Connection<'_>) { + conn.send_request(&SendEvent { + destination: SendEventDest::Window(self.window), + event_mask: EventMask::STRUCTURE_NOTIFY, + event: &ConfigureNotifyEvent::new( + self.window, + self.window, + Window::none(), + self.x, + self.y, + self.width, + self.height, + self.border_width, + false, + ), + propagate: false, + }); + } + + /// Set this client's geometry, also updating the X11 window if needed. + /// This function does not check for success, so `conn.flush()` should be called after. + pub fn set_geom( + &mut self, + conn: &Connection<'_>, + x: i16, + y: i16, + width: u16, + height: u16, + border_width: u16, + ) { + if (x, y, width, height, border_width) + == (self.x, self.y, self.width, self.height, self.border_width) + { + return; + } + + self.x = x; + self.y = y; + self.width = width; + self.height = height; + self.border_width = border_width; + + conn.send_request(&ConfigureWindow { + window: self.window, + value_list: &[ + ConfigWindow::X(self.x.into()), + ConfigWindow::Y(self.y.into()), + ConfigWindow::Width(self.width.into()), + ConfigWindow::Height(self.height.into()), + ConfigWindow::BorderWidth(self.border_width.into()), + ], + }); + } + + /// Set the border colour of the X11 window to the given value (see `crate::colours::Colours`) + /// This function does not check for success, so `conn.flush()` should be called after. + pub fn set_border(&self, conn: &Connection<'_>, colour: u32) { + conn.send_request(&ChangeWindowAttributes { + window: self.window(), + value_list: &[Cw::BorderPixel(colour)], + }); + } + + /// Ensure this client is currently mapped / visible + /// This function does not check for success, so `conn.flush()` should be called after. + pub fn ensure_mapped(&mut self, conn: &Connection<'_>) { + if !self.mapped { + conn.send_request(&MapWindow { + window: self.window, + }); + self.mapped = true; + } + } + + /// Get the associated window + pub const fn window(&self) -> Window { + self.window + } + + /// Set the event mask for this window + /// This function does not check for success, so `conn.flush()` should be called after. + pub fn set_event_mask(&self, conn: &Connection<'_>, event_mask: EventMask) { + conn.send_request(&ChangeWindowAttributes { + window: self.window(), + value_list: &[Cw::EventMask(event_mask)], + }); + } + + /// Sync the non-geometry related properties with EWMH hints + /// This function does not check for success, so `conn.flush()` should be called after. + pub fn sync_properties(&mut self, conn: &Connection<'_>, focused: bool) { + let Some(mut hints) = hints::Xwm::get(conn, self.window) else { + return; + }; + + if focused && hints.is_urgent() { + hints.set_urgent(false); + hints.apply(conn, self.window); + } else { + self.urgent = hints.is_urgent(); + } + + self.never_focus = hints.input().is_some_and(|i| !i); + } + + /// Set the given window as withdrawn / not withdrawn. + pub fn set_withdrawn(&self, conn: &Connection<'_>, withdrawn: bool) { + conn.send_request(&ChangeProperty { + mode: xcb::x::PropMode::Replace, + window: self.window, + property: conn.atoms.wm_state, + r#type: conn.atoms.wm_state, + data: &[u8::from(!withdrawn), 0_u8], + }); + } + + pub fn set_urgent(&mut self, urgent: bool) { + self.urgent = urgent; + } + + pub const fn border_width(&self) -> u16 { + self.border_width + } + + pub const fn never_focus(&self) -> bool { + self.never_focus + } +} diff --git a/src/clients/hints.rs b/src/clients/hints.rs new file mode 100644 index 0000000..0250401 --- /dev/null +++ b/src/clients/hints.rs @@ -0,0 +1,104 @@ +use xcb::{ + x::{self, ChangeProperty, GetProperty, Pixmap, PropMode, Window}, + Xid, XidNew, +}; + +use crate::conn_info::Connection; + +pub struct Xwm { + flags: u32, + input: bool, + initial_state: i32, + icon_pixmap: Pixmap, + icon_window: Window, + icon_x: i32, + icon_y: i32, + icon_mask: Pixmap, + window_group: u32, +} + +impl Xwm { + /// Get the EWM hints for the given window, if they exist and are valid. + pub fn get(conn: &Connection<'_>, window: Window) -> Option { + // https://github.com/mirror/libX11/blob/ff8706a5eae25b8bafce300527079f68a201d27f/src/GetHints.c#L106 + // https://github.com/mirror/libX11/blob/master/src/Xatomtype.h#L111 + let hints = conn + .wait_for_reply(conn.send_request(&GetProperty { + window, + delete: false, + property: x::ATOM_WM_HINTS, + r#type: x::ATOM_WM_HINTS, + long_offset: 0, + long_length: 9, + })) + .ok()?; + + if hints.r#type() != x::ATOM_WM_HINTS || hints.length() < 8 || hints.format() != 32 { + return None; + } + + let [flags, input, initial_state, icon_pixmap, icon_window, icon_x, icon_y, icon_mask, window_group] = + match hints.value::() { + [f, i, is, ip, iw, ix, iy, im, wg] => [f, i, is, ip, iw, ix, iy, im, wg], + [f, i, is, ip, iw, ix, iy, im] => [f, i, is, ip, iw, ix, iy, im, &0], + _ => unreachable!(), + }; + + unsafe { + Some(Self { + flags: *flags, + input: *input > 0, + initial_state: std::mem::transmute::(*initial_state), + icon_pixmap: Pixmap::new(*icon_pixmap), + icon_window: Window::new(*icon_window), + icon_x: std::mem::transmute::(*icon_x), + icon_y: std::mem::transmute::(*icon_y), + icon_mask: Pixmap::new(*icon_mask), + window_group: *window_group, + }) + } + } + + /// Set these WM hints on the given window. + /// This function does not check for success, so `conn.flush()` should be called after. + pub fn apply(&self, conn: &Connection<'_>, window: Window) { + conn.send_request(&ChangeProperty { + mode: PropMode::Replace, + window, + property: x::ATOM_WM_HINTS, + r#type: x::ATOM_WM_HINTS, + data: unsafe { + &[ + self.flags, + u32::from(self.input), + std::mem::transmute::(self.initial_state), + self.icon_pixmap.resource_id(), + self.icon_window.resource_id(), + std::mem::transmute::(self.icon_x), + std::mem::transmute::(self.icon_y), + self.icon_mask.resource_id(), + self.window_group, + ] + }, + }); + } + + /// If the window is flagged as urgent + pub const fn is_urgent(&self) -> bool { + (self.flags & 1 << 8) > 0 + } + + /// Set the urgent flag. [`Self::apply`] should be called afterwards. + pub fn set_urgent(&mut self, urgent: bool) { + self.flags &= u32::from(urgent) << 8; + } + + /// Whether the window has the `input` flag set, unset, or not specified. + pub const fn input(&self) -> Option { + if (self.flags & 1 << 0) > 0 { + Some(self.input) + } else { + None + } + } +} diff --git a/src/clients/mod.rs b/src/clients/mod.rs new file mode 100644 index 0000000..d07de74 --- /dev/null +++ b/src/clients/mod.rs @@ -0,0 +1,425 @@ +use crate::{ + config::BORDER_WIDTH, + conn_info::Connection, + error::{Error, Result}, + WM, +}; +use xcb::{ + x::{ + self, ChangeProperty, ConfigWindow, ConfigureNotifyEvent, ConfigureRequestEvent, + ConfigureWindow, DeleteProperty, DestroyNotifyEvent, Drawable, EventMask, GetGeometry, + GetWindowAttributes, InputFocus, MapRequestEvent, SetInputFocus, UnmapNotifyEvent, Window, + }, + xinerama::{self}, + BaseEvent, Extension, +}; + +pub use client::Client; +pub use monitors::*; + +mod client; +mod hints; +mod monitors; +mod tile; + +impl WM<'_> { + /// Perform configure requests if we're happy with them, or they're for an unmanaged window. + pub(crate) fn handle_configure_request(&mut self, e: &ConfigureRequestEvent) -> Result<()> { + if let Some(c) = self.clients.find_client_mut(e.window()) { + // TODO: Allow changing some properties: + // - Border width + // - Size and position if floating + c.configure_notify(&self.conn); + self.conn.flush()?; + } else { + // Configure it as requested, and sort the rest when we actually map the window + self.conn.send_and_check_request(&ConfigureWindow { + window: e.window(), + value_list: &[ + ConfigWindow::X(e.x().into()), + ConfigWindow::Y(e.y().into()), + ConfigWindow::Width(e.width().into()), + ConfigWindow::Height(e.height().into()), + ConfigWindow::BorderWidth(e.border_width().into()), + ConfigWindow::StackMode(e.stack_mode()), + ], + })?; + } + + Ok(()) + } + + /// Update our monitor geometry if the root window is reconfigured + pub(crate) fn handle_configure_notify(&mut self, e: &ConfigureNotifyEvent) -> Result<()> { + if e.window() == self.conn.root() { + self.clients.update_geometry(&self.conn)?; + self.conn.flush()?; + } + Ok(()) + } + + /// Removing destroyed windows from the client list and rearrange. + pub(crate) fn handle_destroy_notify(&mut self, e: &DestroyNotifyEvent) -> Result<()> { + self.clients.unmanage(&self.conn, e.window(), true); + self.conn.flush()?; + + Ok(()) + } + + /// Map a window, starting to manage it if needed. + pub(crate) fn handle_map_request(&mut self, e: &MapRequestEvent) -> Result<()> { + // Ignore already managed windows + if self.clients.find_client_mut(e.window()).is_some() { + return Ok(()); + } + + let attrs = self.conn.wait_for_reply( + self.conn + .send_request(&GetWindowAttributes { window: e.window() }), + )?; + if attrs.override_redirect() { + // Something special, or us doing actual mapping. Don't manage it just let it do its thing. + return Ok(()); + } + + // Start managing, and map window + self.clients.manage(&self.conn, e.window()); + + self.conn.flush()?; + + Ok(()) + } + + /// When a window is unmapped, either stop managing it or update its state. + pub(crate) fn handle_unmap_notify(&mut self, e: &UnmapNotifyEvent) -> Result<()> { + if let Some(c) = self.clients.find_client_mut(e.window()) { + if e.is_from_send_event() { + c.set_withdrawn(&self.conn, true); + } else { + self.clients.unmanage(&self.conn, e.window(), false); + } + self.conn.flush()?; + } + + Ok(()) + } +} + +/// Holds state related to the window manager's clients +/// This contains a list of clients per monitor, alongside info on that monitor's screen size. +pub struct ClientState { + /// The current arranging function. + /// This function is expected to ensure that all clients are the correct size, reconfigure them if needed, and map/unmap as needed. + /// The connection will be flushed after it is called. + arrange: &'static dyn Fn(&mut MonitorInfo, &Connection<'_>), + + /// A client list for each monitor. + mons: Vec, + + /// Co-ordinates to the currently focused window. + focused: (usize, usize), +} + +impl ClientState { + /// Update the recorded monitors and monitor sizes, retiling if necessary. + /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. + pub fn update_geometry(&mut self, conn: &Connection<'_>) -> Result<()> { + let mut dirty = false; + if conn.active_extensions().any(|e| e == Extension::Xinerama) { + let reply = conn.wait_for_reply(conn.send_request(&xinerama::QueryScreens {}))?; + + // Monitor removed, move its clients away + if reply.screen_info().len() > self.monitor_count() { + dirty = true; + self.truncate_screens(reply.screen_info().len()); + } + + // Update screen info & add new client lists if needed + for (i, monitor) in reply.screen_info().iter().enumerate() { + dirty |= self.set_monitor_geometry(i, (*monitor).into()); + } + } else { + // Only one screen + if self.monitor_count() > 1 { + dirty = true; + self.truncate_screens(1); + } + + // TODO: it looks like this won't actually update when the screen size changes? + let setup = conn.get_setup(); + let screen = setup + .roots() + .nth(conn.screen_num()) + .ok_or(Error::NoSuchScreen)?; + + dirty |= self.set_monitor_geometry( + 0, + MonitorGeometry { + x_org: 0, + y_org: 0, + width: screen.width_in_pixels(), + height: screen.height_in_pixels(), + }, + ); + } + + if dirty { + self.rearrange(conn); + } + Ok(()) + } + + /// Start managing the given window, adding it to the client list and ensuring its configuration is valid. + /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. + pub fn manage(&mut self, conn: &Connection<'_>, window: Window) { + // TODO: inherit from parent if window is transient + let mon = self.focused_mon(); + + let Ok(geom) = conn.wait_for_reply(conn.send_request(&GetGeometry { + drawable: Drawable::Window(window), + })) else { + return; // window stopped existing, so we can't manage it + }; + + // We're about to invalidate focus position + self.unfocus(conn); + // TODO: inserting at index 0 is why dwm uses linked lists, maybe this can be improved + self.mons[mon].clients.insert(0, Client::new(window)); + + // TODO: Clamp window size to monitor + let c = &mut self.mons[mon].clients[0]; + c.set_geom( + conn, + geom.x(), + geom.y(), + geom.width(), + geom.height(), + BORDER_WIDTH, + ); + c.set_border(conn, conn.colours.border_normal()); + c.ensure_mapped(conn); + + // TODO: updatewindowtype + // TODO: updatesizehints + c.sync_properties(conn, true); + + c.set_event_mask( + conn, + EventMask::ENTER_WINDOW + | EventMask::FOCUS_CHANGE + | EventMask::PROPERTY_CHANGE + | EventMask::STRUCTURE_NOTIFY, + ); + + // TODO: grabbuttons + + // Add to net_client_list + conn.send_request(&ChangeProperty { + mode: xcb::x::PropMode::Append, + window: conn.root(), + property: conn.atoms.net_client_list, + r#type: x::ATOM_WINDOW, + data: &[window], + }); + c.set_withdrawn(conn, false); + + // TODO: XMoveResizeWindow(dpy, c->win, c->x + 2 * sw, c->y, c->w, c->h); /* some windows require this */ + self.refocus(conn, mon, 0); + self.rearrange_monitor(conn, mon); + } + + /// Stop managing the given window, and also unset attributes unless `already_destroyed` is true. + /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. + pub fn unmanage(&mut self, conn: &Connection<'_>, window: Window, already_destroyed: bool) { + let Some((mon, i)) = self.find_client_pos(window) else { + return; + }; + let c = self.mons[mon].clients.remove(i); + + if !already_destroyed { + c.set_event_mask(conn, EventMask::empty()); + // TODO: Ungrab button + c.set_withdrawn(conn, true); + } + self.rearrange(conn); + } + + /// Refocus on the client with the given co-ordinates, setting X11 properties as required. + /// If the given index is invalid, focus on the root instead. + /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. + pub fn refocus(&mut self, conn: &Connection<'_>, mon: usize, i: usize) { + self.unfocus(conn); + if let Some(new) = self.set_focused(mon, i) { + new.set_border(conn, conn.colours.border_focused()); + new.set_urgent(false); + if !new.never_focus() { + // XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime); + conn.send_request(&SetInputFocus { + revert_to: InputFocus::PointerRoot, + focus: new.window(), + time: x::CURRENT_TIME, + }); + // XChangeProperty(dpy, root, netatom[NetActiveWindow], + // XA_WINDOW, 32, PropModeReplace, + // (unsigned char *) &(c->win), 1); + // TODO: sendevent(c, wmatom[WMTakeFocus]); + } + } else { + conn.send_request(&SetInputFocus { + revert_to: InputFocus::PointerRoot, + focus: conn.root(), + time: x::CURRENT_TIME, + }); + conn.send_request(&DeleteProperty { + window: conn.root(), + property: conn.atoms.net_active_window, + }); + } + } + + /// Unfocus the currently focused window, if it exists. + /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. + pub fn unfocus(&mut self, conn: &Connection<'_>) { + if let Some(old) = self.focused_mut() { + old.set_border(conn, conn.colours.border_normal()); + // TODO: clear some properties + } + } + + /// Get the amount of monitors this state is currently aware of + pub fn monitor_count(&self) -> usize { + self.mons.len() + } + + /// Set the new amount of screens, without unmanaging any clients. + fn truncate_screens(&mut self, new_size: usize) { + // hack: working around double borrow stuff + let mut moved_clients = vec![]; + for old in self.mons.drain(new_size - self.mons.len()..self.mons.len()) { + moved_clients.extend(old.clients.into_iter()); + } + self.mons[0].clients.extend(moved_clients); + } + + /// Set the given screen's geometry, resizing the monitor list if necessary. + /// Returns true if the new info is different from the old one. + fn set_monitor_geometry(&mut self, i: usize, info: MonitorGeometry) -> bool { + while i >= self.mons.len() { + self.mons.push(MonitorInfo::default()); + } + let dirty = self.mons[i].screen_info != info; + self.mons[i].screen_info = info; + + dirty + } + + /// Find the [`Client`] corresponding to the given window + pub fn find_client_mut(&mut self, window: Window) -> Option<&mut Client> { + let (mon, i) = self.find_client_pos(window)?; + Some(&mut self.mons[mon].clients[i]) + } + + /// Find the position of the client with the given window, returning (monitor, index) + pub fn find_client_pos(&mut self, window: Window) -> Option<(usize, usize)> { + for (pos_mon, mon) in self.mons.iter_mut().enumerate() { + if let Some(pos) = mon.clients.iter().position(|c| c.window() == window) { + return Some((pos_mon, pos)); + } + } + + None + } + + /// Get a reference to the currently focused client, if it exists. + pub fn focused(&self) -> Option<&Client> { + self.client(self.focused.0, self.focused.1) + } + + /// Get a mutable reference to the currently focused client, if it exists. + pub fn focused_mut(&mut self) -> Option<&mut Client> { + self.client_mut(self.focused.0, self.focused.1) + } + + /// Get the position of the currently focused client. This position may be invalid. + pub fn is_focused(&self, e: Window) -> bool { + self.focused().is_some_and(|c| c.window() == e) + } + + /// Set the currently focused client, returning a mutable reference to it if the co-ordinates are valid. + pub fn set_focused(&mut self, mut mon: usize, mut i: usize) -> Option<&mut Client> { + if self.mons.is_empty() { + return None; + } + + if mon >= self.mons.len() { + mon = self.mons.len() - 1; + } + + if self.mons[mon].clients.is_empty() { + return None; + } + + if i >= self.mons[mon].clients.len() { + i = self.mons[mon].clients.len() - 1; + } + + self.focused = (mon, i); + Some(&mut self.mons[mon].clients[i]) + } + + /// Get a mutable reference to the client at the given co-ordinates, if they are valid. + pub fn client(&self, mon: usize, i: usize) -> Option<&Client> { + if mon < self.mons.len() && i < self.mons[mon].clients.len() { + Some(&self.mons[mon].clients[i]) + } else { + None + } + } + + /// Get a mutable reference to the client at the given co-ordinates, if they are valid. + pub fn client_mut(&mut self, mon: usize, i: usize) -> Option<&mut Client> { + if mon < self.mons.len() && i < self.mons[mon].clients.len() { + Some(&mut self.mons[mon].clients[i]) + } else { + None + } + } + + /// Rearrange all clients, reconfiguring them as needed. + /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. + pub fn rearrange(&mut self, conn: &Connection<'_>) { + for mon in 0..self.monitor_count() { + self.rearrange_monitor(conn, mon); + } + } + + /// Rearrange a specific monitor + /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. + fn rearrange_monitor(&mut self, conn: &Connection<'_>, mon: usize) { + (self.arrange)(&mut self.mons[mon], conn); + } + + /// Get the currently focused monitor + const fn focused_mon(&self) -> usize { + self.focused.0 + } +} + +impl Default for ClientState { + fn default() -> Self { + Self { + arrange: &tile::tile, + focused: (0, 0), + mons: vec![], + } + } +} + +impl std::fmt::Debug for ClientState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClientState") + .field("focused", &self.focused) + .field("mons", &self.mons) + .finish_non_exhaustive() + } +} diff --git a/src/clients/monitors.rs b/src/clients/monitors.rs new file mode 100644 index 0000000..0a9bcde --- /dev/null +++ b/src/clients/monitors.rs @@ -0,0 +1,55 @@ +use xcb::xinerama::ScreenInfo; + +use super::Client; + +/// Info stored for each monitor +#[derive(Debug)] +pub struct MonitorInfo { + /// Clients attached to that monitor + pub clients: Vec, + + /// The monitor's geometry + pub screen_info: MonitorGeometry, +} + +impl MonitorInfo { + /// Iterate over all tiled clients, returning a mutable reference to each. + pub fn clients_tiled_mut(&mut self) -> impl Iterator { + // TODO: tag filtering, floating + self.clients.iter_mut() + } +} + +impl Default for MonitorInfo { + fn default() -> Self { + Self { + clients: vec![], + screen_info: MonitorGeometry { + x_org: 0, + y_org: 0, + width: 0, + height: 0, + }, + } + } +} + +/// Info on the monitor's geometry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MonitorGeometry { + pub x_org: i16, + pub y_org: i16, + pub width: u16, + pub height: u16, +} + +impl From for MonitorGeometry { + fn from(value: ScreenInfo) -> Self { + Self { + x_org: value.x_org, + y_org: value.y_org, + width: value.width, + height: value.height, + } + } +} diff --git a/src/clients/tile.rs b/src/clients/tile.rs new file mode 100644 index 0000000..4a2d4fc --- /dev/null +++ b/src/clients/tile.rs @@ -0,0 +1,68 @@ +use std::cmp::min; + +use crate::conn_info::Connection; + +use super::{MonitorGeometry, MonitorInfo}; + +/// A simple tiling function +#[allow( + clippy::cast_sign_loss, + clippy::cast_possible_wrap, + clippy::cast_possible_truncation, + clippy::cast_lossless +)] +pub fn tile(mon: &mut MonitorInfo, conn: &Connection<'_>) { + if mon.clients.is_empty() { + return; + } + + let n = mon.clients_tiled_mut().count(); + let nmaster = 1; + let mfact = 0.6; + + let MonitorGeometry { + x_org, + y_org, + width: mon_width, + height: mon_height, + } = mon.screen_info; + + let main_width = if nmaster == 0 { + 0 + } else if n > nmaster { + ((mon.screen_info.width as f64) * mfact) as u16 + } else { + mon.screen_info.width + }; + + let (mut main_y, mut second_y) = (0, 0); + for (i, c) in mon.clients_tiled_mut().enumerate() { + if i < nmaster { + let h = (mon_height - main_y) / (min(nmaster, n) - i) as u16; + c.set_geom( + conn, + x_org, + y_org + main_y as i16, + main_width - (2 * c.border_width()), + h - (2 * c.border_width()), + c.border_width(), + ); + + main_y += h; + } else { + let h = (mon_height - second_y) / (n - i) as u16; + c.set_geom( + conn, + x_org + main_width as i16, + y_org + second_y as i16, + mon_width - main_width - (2 * c.border_width()), + h - (2 * c.border_width()), + c.border_width(), + ); + + second_y += h; + } + + c.ensure_mapped(conn); + } +} diff --git a/src/colours.rs b/src/colours.rs deleted file mode 100644 index bab92b6..0000000 --- a/src/colours.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::error::*; -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 { - // 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 fn border_normal(&self) -> u32 { - self.border_normal - } - - /// Get the pixel ID of the colour for a focused window's border. - pub fn border_focused(&self) -> u32 { - self.border_focused - } -} 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 { + // 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 { + // 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 { + 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, + + /// 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 { + 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 + '_ { + (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 { + 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 { + 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 { + // 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(&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(&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 { + self.conn.wait_for_event().map_err(Into::into) + } + + pub fn active_extensions(&self) -> impl Iterator + '_ { + self.conn.active_extensions() + } + + pub fn wait_for_reply(&self, cookie: C) -> xcb::Result + 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!() + } +} diff --git a/src/cursors.rs b/src/cursors.rs deleted file mode 100644 index 0a4e041..0000000 --- a/src/cursors.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::error::*; -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 { - // Open cursor font - let font = conn.generate_id(); - conn.check_request(conn.send_request_checked(&OpenFont { - fid: font, - name: "cursor".as_bytes(), - }))?; - - 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 { - 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 fn normal(&self) -> Cursor { - self.normal - } -} diff --git a/src/error.rs b/src/error.rs index dcbcfe9..d0253af 100644 --- a/src/error.rs +++ b/src/error.rs @@ -27,11 +27,11 @@ impl std::error::Error for Error {} impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Error::NoSuchScreen => write!(f, "xcb returned a screen that doesn't exist"), - Error::OtherWMRunning => write!(f, "another window manager is running"), - Error::Xcb(e) => write!(f, "generic xcb error: {}", e), - Error::Connection(e) => write!(f, "connection error: {}", e), - Error::Protocol(e) => write!(f, "protocol error: {}", e), + Self::NoSuchScreen => write!(f, "xcb returned a screen that doesn't exist"), + Self::OtherWMRunning => write!(f, "another window manager is running"), + Self::Xcb(e) => write!(f, "generic xcb error: {e}"), + Self::Connection(e) => write!(f, "connection error: {e}"), + Self::Protocol(e) => write!(f, "protocol error: {e}"), } } } diff --git a/src/focus.rs b/src/focus.rs index b26381e..0c9512f 100644 --- a/src/focus.rs +++ b/src/focus.rs @@ -1,80 +1,33 @@ -use xcb::x::{ - self, DeleteProperty, EnterNotifyEvent, FocusInEvent, InputFocus, NotifyDetail, NotifyMode, - SetInputFocus, Window, -}; +use xcb::x::{EnterNotifyEvent, FocusInEvent, NotifyDetail, NotifyMode}; -use crate::{error::*, WM}; +use crate::{error::Result, WM}; impl WM<'_> { /// When a new window is entered, focus it. - pub fn handle_enter_notify(&mut self, e: EnterNotifyEvent) -> Result<()> { + pub fn handle_enter_notify(&mut self, e: &EnterNotifyEvent) -> Result<()> { if (e.mode() != NotifyMode::Normal || e.detail() == NotifyDetail::Inferior) - && e.event() != self.root + && e.event() != self.conn.root() { return Ok(()); } - self.focus_window(e.event()); - self.conn.flush()?; - - Ok(()) - } - - /// When a new window requests focus, focus it. - pub fn handle_focus_in(&mut self, e: FocusInEvent) -> Result<()> { - if self - .clients - .focused_mut() - .map(|c| c.window() != e.event()) - .unwrap_or(true) - { - self.focus_window(e.event()); + if let Some((mon, pos)) = self.clients.find_client_pos(e.event()) { + self.clients.refocus(&self.conn, mon, pos); self.conn.flush()?; } Ok(()) } - /// Attempt to focus the given window, even if it isn't managed. - /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. - pub fn focus_window(&mut self, window: Window) { - if let Some((mon, i)) = self.clients.find_client_pos(window) { - self.refocus(mon, i); - } else { - self.conn.send_request(&SetInputFocus { - revert_to: InputFocus::PointerRoot, - focus: window, - time: x::CURRENT_TIME, - }); - self.conn.send_request(&DeleteProperty { - window, - property: self.atoms.net_active_window, - }); - } - } - - /// Refocus on the client with the given co-ordinates, setting X11 properties as required. - /// If the given index is invalid, focus on the root instead. - /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. - pub fn refocus(&mut self, mon: usize, i: usize) { - self.unfocus(); - if let Some(new) = self.clients.set_focused(mon, i) { - new.set_border(self.conn, self.colours.border_focused()); - // TODO: reset urgent flag - // TODO: something to do with grabbuttons - // TODO: set input focus - // TODO: set active window - // TODO: send wmtakefocus event - } else { - // TODO: focus on root + /// When a new window requests focus, focus it. + pub fn handle_focus_in(&mut self, e: &FocusInEvent) -> Result<()> { + if !self.clients.is_focused(e.event()) { + if let Some((mon, pos)) = self.clients.find_client_pos(e.event()) { + self.clients.refocus(&self.conn, mon, pos); + self.conn.flush()?; + } } - } - /// Unfocus the currently focused window, if it exists. - /// This function sends multiple requests without checking them, so `conn.flush()` should be called after. - pub fn unfocus(&mut self) { - if let Some(old) = self.clients.focused_mut() { - old.set_border(self.conn, self.colours.border_normal()); - } + Ok(()) } } diff --git a/src/keys.rs b/src/keys.rs index 21217b0..08cd46d 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,177 +1,79 @@ -use std::ops::RangeInclusive; - -use crate::{config::KEYBINDS, error::*, WM}; -use xcb::{ - x::{ - GetKeyboardMapping, GetKeyboardMappingReply, GetModifierMapping, GrabKey, GrabMode, - KeyPressEvent, Mapping, MappingNotifyEvent, ModMask, UngrabKey, GRAB_ANY, - }, - Connection, +use crate::{config::KEYBINDS, conn_info::Connection, error::Result, WM}; +use xcb::x::{ + GrabKey, GrabMode, KeyPressEvent, Mapping, MappingNotifyEvent, ModMask, UngrabKey, GRAB_ANY, }; -use xkeysym::{KeyCode, Keysym, RawKeyCode}; +use xkeysym::Keysym; impl WM<'_> { /// Dispatch the given keypress event according to [`self::KEYBINDS`] - pub fn handle_key_press(&mut self, e: KeyPressEvent) -> Result<()> { - let Some(sym) = self.keyboard_state.keycode_to_keysym(e.detail().into(), 0) else { - return Ok(()); // probably not bound + pub fn handle_key_press(&mut self, e: &KeyPressEvent) { + let Some(sym) = self + .conn + .keyboard_state + .keycode_to_keysym(e.detail().into(), 0) + else { + return; // probably not bound }; KEYBINDS.dispatch( self, sym, ModMask::from_bits_truncate(e.state().bits()) - .difference(self.keyboard_state.numlock_mask() | ModMask::LOCK), + .difference(self.conn.keyboard_state.numlock_mask() | ModMask::LOCK), ); - - Ok(()) } /// Update our keyboard info when the mapping changes. - pub fn handle_mapping_notify(&mut self, e: MappingNotifyEvent) -> Result<()> { + pub fn handle_mapping_notify(&mut self, e: &MappingNotifyEvent) -> Result<()> { if e.request() == Mapping::Keyboard { - self.grab_keys()?; + grab_keys(&mut self.conn)?; } Ok(()) } - - /// Refresh our keyboard info, and ensure that we get events for bound keys. - pub fn grab_keys(&mut self) -> Result<()> { - // Refresh keyboard state - self.keyboard_state = KeyboardInfo::new_with(self.conn)?; - - // Ungrab all keys - self.conn.send_request(&UngrabKey { - key: GRAB_ANY, - grab_window: self.root, - modifiers: ModMask::ANY, - }); - - // Bind all of the keycodes which have keysyms we see in our binds. - for (keycode, keysym) in self.keyboard_state.iter_keycodes_keysyms() { - for bind in KEYBINDS.binds() { - if bind.key == keysym { - // grab key with any combination of modifiers - for modmask in [ - ModMask::empty(), - ModMask::LOCK, - self.keyboard_state.numlock_mask(), - self.keyboard_state.numlock_mask() | ModMask::LOCK, - ] { - self.conn.send_request(&GrabKey { - grab_window: self.root, - key: keycode.raw() as u8, - modifiers: bind.modifiers | modmask, - owner_events: true, - pointer_mode: GrabMode::Async, - keyboard_mode: GrabMode::Async, - }); - } - } - } - } - - // Ensure all requests succeeded - self.conn.flush()?; - - Ok(()) - } -} - -/// Cached information about our keyboard layout. -pub struct KeyboardInfo { - /// The range of keycodes used - keycodes: RangeInclusive, - - /// 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 { - 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: min_keycode as RawKeyCode..=max_keycode as RawKeyCode, - 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 mod_map.keycodes()[i * keypermod + j] as u32 == numlock_keycode.raw() { - this.numlock_mask = - ModMask::from_bits(1 << i).expect("x11 returned unrecognised modifier"); +/// Refresh our keyboard info, and ensure that we get events for bound keys. +fn grab_keys(conn: &mut Connection<'_>) -> Result<()> { + // Refresh keyboard state + conn.refresh_keyboard_info()?; + + // Ungrab all keys + conn.send_request(&UngrabKey { + key: GRAB_ANY, + grab_window: conn.root(), + modifiers: ModMask::ANY, + }); + + // Bind all of the keycodes which have keysyms we see in our binds. + for (keycode, keysym) in conn.keyboard_state.iter_keycodes_keysyms() { + for bind in KEYBINDS.binds() { + if bind.key == keysym { + // grab key with any combination of modifiers + for modmask in [ + ModMask::empty(), + ModMask::LOCK, + conn.keyboard_state.numlock_mask(), + conn.keyboard_state.numlock_mask() | ModMask::LOCK, + ] { + conn.send_request(&GrabKey { + grab_window: conn.root(), + #[allow(clippy::cast_possible_truncation)] + key: keycode.raw() as u8, + modifiers: bind.modifiers | modmask, + owner_events: true, + pointer_mode: GrabMode::Async, + keyboard_mode: GrabMode::Async, + }); } } } - - Ok(this) - } - - /// Get the modifier mask being used for numlock - pub 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 + '_ { - (0..self.mapping.keysyms_per_keycode()) - .flat_map(|shift| self.keycodes.clone().map(move |keycode| (shift, keycode))) - .flat_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 { - 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 - } + // Ensure all requests succeeded + conn.flush()?; - /// Lookup the keysym in the given column for the given keycode - pub fn keycode_to_keysym(&self, keycode: KeyCode, col: u8) -> Option { - xkeysym::keysym( - keycode, - col, - (*self.keycodes.start()).into(), - self.mapping.keysyms_per_keycode(), - self.mapping.keysyms(), - ) - } + Ok(()) } /// A key bound to some action @@ -206,6 +108,6 @@ impl std::fmt::Debug for Keybind { f.debug_struct("Keybind") .field("modifiers", &self.modifiers) .field("key", &self.key) - .finish() + .finish_non_exhaustive() } } diff --git a/src/main.rs b/src/main.rs index 374b403..0232e61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,17 @@ //! A lightweight X11 window manager, inspired by dwm. +#![deny(clippy::all, clippy::pedantic, clippy::nursery)] -use atoms::Atoms; use clients::ClientState; -use colours::Colours; -use cursors::Cursors; -use error::*; -use keys::KeyboardInfo; +use conn_info::Connection; +pub use error::*; use xcb::{ - x::{ - self, ChangeProperty, ChangeWindowAttributes, CreateWindow, DeleteProperty, - PropertyNotifyEvent, Window, WindowClass, - }, - Connection, Event, Extension, + x::{self, PropertyNotifyEvent}, + Connection as RawConnection, Event, Extension, }; -mod atoms; mod clients; -mod colours; mod config; -mod cursors; +mod conn_info; mod error; mod focus; mod keys; @@ -27,211 +20,62 @@ fn main() -> Result<()> { cleanup_process_children(); let (conn, screen_num) = - Connection::connect_with_extensions(None, &[], &[Extension::Xinerama])?; + RawConnection::connect_with_extensions(None, &[], &[Extension::Xinerama])?; - let mut wm = WM::new(&conn, screen_num)?; + #[allow(clippy::cast_sign_loss)] + let mut wm = WM::new(&conn, screen_num as usize)?; wm.event_loop()?; Ok(()) } -/// The window manager's state struct WM<'a> { - /// The open connection to an X server - conn: &'a Connection, - - /// The 'screen' number on the X server - /// Note this isn't what you think it is on multi-monitor setups - screen_num: i32, - - /// 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) - check_window: Window, - - /// WM client state + conn: Connection<'a>, clients: ClientState, - - /// Cached colours, - colours: Colours, - - /// Cached cursors - cursors: Cursors, - - /// Cached atoms - atoms: Atoms, - - /// Cached keyboard layout information - keyboard_state: KeyboardInfo, } -impl WM<'_> { - /// Prepare the window manager to run on the given connection and screen number. - /// This will fail if another WM is running. - fn new(conn: &'_ Connection, screen_num: i32) -> Result> { - // Fetch root window - let setup = conn.get_setup(); - let screen = setup - .roots() - .nth(screen_num as usize) - .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)?; - - Ok(WM { - colours: Colours::new_with(conn, screen.default_colormap())?, - atoms: Atoms::intern_all(conn)?, - cursors: Cursors::new_with(conn)?, - keyboard_state: KeyboardInfo::new_with(conn)?, - clients: Default::default(), - check_window: conn.generate_id(), - conn, - screen_num, - root: screen.root(), +impl<'a> WM<'a> { + pub fn new(conn: &'a RawConnection, screen_num: usize) -> Result { + Ok(Self { + conn: Connection::new(conn, screen_num)?, + clients: ClientState::default(), }) } - /// Set the correct properties on the root window - fn setup_root(&mut self) -> Result<()> { - // Check window - self.conn.send_request(&CreateWindow { - wid: self.check_window, - parent: self.root, - depth: 0, - x: 0, - y: 0, - width: 0, - height: 0, - border_width: 0, - class: WindowClass::InputOutput, - visual: 0, - value_list: &[], - }); - - self.conn.send_request(&ChangeProperty { - mode: x::PropMode::Replace, - window: self.root, - property: self.atoms.net_wm_check, - r#type: x::ATOM_WINDOW, - data: &[self.check_window], - }); - - self.conn.send_request(&ChangeProperty { - mode: x::PropMode::Replace, - window: self.check_window, - property: self.atoms.net_wm_check, - r#type: x::ATOM_WINDOW, - data: &[self.check_window], - }); - - self.conn.send_request(&ChangeProperty { - mode: x::PropMode::Replace, - window: self.check_window, - property: self.atoms.net_wm_name, - r#type: x::ATOM_STRING, - data: b"blow", - }); - - // Supported flag - self.conn.send_request(&ChangeProperty { - mode: x::PropMode::Replace, - window: self.root, - property: self.atoms.net_supported, - r#type: x::ATOM_ATOM, - data: &[ - self.atoms.net_active_window, - self.atoms.net_wm_name, - self.atoms.net_wm_state, - self.atoms.net_wm_check, - self.atoms.net_wm_fullscreen, - self.atoms.net_wm_window_type, - self.atoms.net_wm_window_type_dialog, - self.atoms.net_client_list, - ], - }); - - // Cleanup state - self.conn.send_request(&DeleteProperty { - window: self.root, - property: self.atoms.net_client_list, - }); - - // Get the right events - self.conn.send_request(&ChangeWindowAttributes { - window: self.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(self.cursors.normal()), - ], - }); - - self.grab_keys()?; - - self.refocus(usize::MAX, usize::MAX); - self.conn.flush()?; - - Ok(()) - } - - fn event_loop(&mut self) -> Result<()> { - // Perform setup - self.update_geometry()?; - self.setup_root()?; - + pub fn event_loop(&mut self) -> Result<()> { loop { match self.conn.wait_for_event()? { // See keys.rs - Event::X(x::Event::KeyPress(e)) => self.handle_key_press(e)?, - Event::X(x::Event::MappingNotify(e)) => self.handle_mapping_notify(e)?, - - // See clients.rs - Event::X(x::Event::ConfigureRequest(e)) => self.handle_configure_request(e)?, - Event::X(x::Event::ConfigureNotify(e)) => self.handle_configure_notify(e)?, - Event::X(x::Event::DestroyNotify(e)) => self.handle_destroy_notify(e)?, - Event::X(x::Event::MapRequest(e)) => self.handle_map_request(e)?, - Event::X(x::Event::UnmapNotify(e)) => self.handle_unmap_notify(e)?, - - // See focus.rs - Event::X(x::Event::EnterNotify(e)) => self.handle_enter_notify(e)?, - Event::X(x::Event::FocusIn(e)) => self.handle_focus_in(e)?, - - // See below - Event::X(x::Event::PropertyNotify(e)) => self.handle_property_notify(e)?, + Event::X(x::Event::KeyPress(e)) => self.handle_key_press(&e), + Event::X(x::Event::MappingNotify(e)) => self.handle_mapping_notify(&e)?, + + // See clients/mod.rs + Event::X(x::Event::ConfigureRequest(e)) => self.handle_configure_request(&e)?, + Event::X(x::Event::ConfigureNotify(e)) => self.handle_configure_notify(&e)?, + Event::X(x::Event::DestroyNotify(e)) => self.handle_destroy_notify(&e)?, + Event::X(x::Event::MapRequest(e)) => self.handle_map_request(&e)?, + Event::X(x::Event::UnmapNotify(e)) => self.handle_unmap_notify(&e)?, + + // // See focus.rs + Event::X(x::Event::EnterNotify(e)) => self.handle_enter_notify(&e)?, + Event::X(x::Event::FocusIn(e)) => self.handle_focus_in(&e)?, + + // // See below + Event::X(x::Event::PropertyNotify(e)) => self.handle_property_notify(&e)?, _ => {} }; } } /// Handle a property notify event, by doing *todo* - fn handle_property_notify(&mut self, e: PropertyNotifyEvent) -> Result<()> { + fn handle_property_notify(&mut self, e: &PropertyNotifyEvent) -> Result<()> { match e.atom() { x::ATOM_WM_HINTS => { - let Some(p) = self.clients.find_client_pos(e.window()) else { - return Ok(()); - }; - let focused = p == self.clients.focused(); - self.clients.client_mut(p.0, p.1).and_then(|c| { - c.sync_properties(self.conn, focused); - Some(()) - }); + let focused = self.clients.is_focused(e.window()); + if let Some(c) = self.clients.find_client_mut(e.window()) { + c.sync_properties(&self.conn, focused); + self.conn.flush()?; + } Ok(()) } @@ -245,15 +89,5 @@ impl WM<'_> { fn cleanup_process_children() { // TODO: dont transform children into zombies when they terminate // TODO: cleanup zombies -} - -impl<'a> std::fmt::Debug for WM<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WM") - .field("screen_num", &self.screen_num) - .field("root", &self.root) - .field("clients", &self.clients) - .field("atoms", &self.atoms) - .finish() - } + todo!() } -- cgit v1.2.3