summaryrefslogtreecommitdiff
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
parentc3e98e34ed7d42ef4271339de88f7131e7647442 (diff)
refactor connection-related stuff out, make things a bit cleaner
-rw-r--r--src/clients.rs710
-rw-r--r--src/clients/client.rs174
-rw-r--r--src/clients/hints.rs104
-rw-r--r--src/clients/mod.rs425
-rw-r--r--src/clients/monitors.rs55
-rw-r--r--src/clients/tile.rs68
-rw-r--r--src/conn_info/atoms.rs (renamed from src/atoms.rs)0
-rw-r--r--src/conn_info/colours.rs (renamed from src/colours.rs)6
-rw-r--r--src/conn_info/cursors.rs (renamed from src/cursors.rs)6
-rw-r--r--src/conn_info/keys.rs104
-rw-r--r--src/conn_info/mod.rs225
-rw-r--r--src/error.rs10
-rw-r--r--src/focus.rs75
-rw-r--r--src/keys.rs198
-rw-r--r--src/main.rs244
15 files changed, 1269 insertions, 1135 deletions
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<bool> {
- 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<MonitorInfo>,
-
- /// 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<Client>,
-
- /// 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<Item = &mut Client> {
- // 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<Client> {
- 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<Self> {
- // 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::<u32>() {
- [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::<u32, i32>(*initial_state),
- icon_pixmap: Pixmap::new(*icon_pixmap),
- icon_window: Window::new(*icon_window),
- icon_x: std::mem::transmute::<u32, i32>(*icon_x),
- icon_y: std::mem::transmute::<u32, i32>(*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<ScreenInfo> 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<Self> {
+ // 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::<u32>() {
+ [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::<u32, i32>(*initial_state),
+ icon_pixmap: Pixmap::new(*icon_pixmap),
+ icon_window: Window::new(*icon_window),
+ icon_x: std::mem::transmute::<u32, i32>(*icon_x),
+ icon_y: std::mem::transmute::<u32, i32>(*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::<i32, u32>(self.initial_state),
+ self.icon_pixmap.resource_id(),
+ self.icon_window.resource_id(),
+ std::mem::transmute::<i32, u32>(self.icon_x),
+ std::mem::transmute::<i32, u32>(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<bool> {
+ 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<MonitorInfo>,
+
+ /// 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<Client>,
+
+ /// 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<Item = &mut Client> {
+ // 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<ScreenInfo> 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/atoms.rs b/src/conn_info/atoms.rs
index 2d23c81..2d23c81 100644
--- a/src/atoms.rs
+++ b/src/conn_info/atoms.rs
diff --git a/src/colours.rs b/src/conn_info/colours.rs
index bab92b6..22b4fc2 100644
--- a/src/colours.rs
+++ b/src/conn_info/colours.rs
@@ -1,4 +1,4 @@
-use crate::error::*;
+use crate::error::Result;
use xcb::{
x::{AllocColor, Colormap},
Connection,
@@ -39,12 +39,12 @@ impl Colours {
}
/// Get the pixel ID of the colour for an unfocused window's border.
- pub fn border_normal(&self) -> u32 {
+ pub const 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 {
+ pub const fn border_focused(&self) -> u32 {
self.border_focused
}
}
diff --git a/src/cursors.rs b/src/conn_info/cursors.rs
index 0a4e041..119de5c 100644
--- a/src/cursors.rs
+++ b/src/conn_info/cursors.rs
@@ -1,4 +1,4 @@
-use crate::error::*;
+use crate::error::Result;
use xcb::{
x::{CreateGlyphCursor, Cursor, Font, OpenFont},
Connection,
@@ -23,7 +23,7 @@ impl Cursors {
let font = conn.generate_id();
conn.check_request(conn.send_request_checked(&OpenFont {
fid: font,
- name: "cursor".as_bytes(),
+ name: b"cursor",
}))?;
Ok(Self {
@@ -53,7 +53,7 @@ impl Cursors {
Ok(cid)
}
- pub fn normal(&self) -> Cursor {
+ 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!()
+ }
+}
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<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: 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<Item = (KeyCode, Keysym)> + '_ {
- (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<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
- }
+ // 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<Keysym> {
- 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<WM<'_>> {
- // 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<Self> {
+ 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!()
}