//! A lightweight X11 window manager, inspired by dwm. //! //! # Structure //! //! The main thing a WM has to do is respond to events: this is done in the [`WM::event_loop`] function, which dispatches to the `handle_*` methods of that struct. //! //! [`conn_info`] wraps XCB's [`xcb::Connection`] type and caches common resources such as atoms, colours, cursors, and keyboard layout info. //! //! `focus.rs`, [`keys`], and [`clients`] all add some event handlers to [`WM`], but most of the important code is in [`clients`]. //! //! [`config`] holds all of the variables that a user might want to change. //! //! # XCB //! //! Unlike dwm, blow uses XCB rather than Xlib. This means requests are asynchronous by default. //! In most places, we avoid checking for errors unless we need to see the response to a request. //! Errors will be caught and logged in the event loop instead. See [`xcb`] documentation for more details. #![deny(clippy::all, clippy::pedantic, clippy::nursery)] #![allow(clippy::must_use_candidate, clippy::missing_errors_doc)] use clients::ClientState; use conn_info::Connection; pub use error::*; use nix::{ sys::{ signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal}, wait::{waitpid, WaitPidFlag}, }, unistd::Pid, }; use xcb::{ x::{self, ClientMessageEvent, PropertyNotifyEvent}, Connection as RawConnection, Event, Extension, Xid, }; pub mod buttons; pub mod clients; pub mod config; pub mod conn_info; #[doc(hidden)] mod error; #[doc(hidden)] mod focus; pub mod helpers; pub mod keys; pub mod log; fn main() -> Result<()> { cleanup_process_children(); let (conn, screen_num) = RawConnection::connect_with_extensions(None, &[], &[Extension::Xinerama])?; #[allow(clippy::cast_sign_loss)] let mut wm = WM::new(&conn, screen_num as usize)?; wm.event_loop()?; Ok(()) } /// All of the state used by the window manager pub struct WM<'a> { conn: Connection<'a>, clients: ClientState, } impl<'a> WM<'a> { /// Prepare to start the window manager, using the given connection and scren number. pub fn new(conn: &'a RawConnection, screen_num: usize) -> Result { let mut this = Self { conn: Connection::new(conn, screen_num)?, clients: ClientState::default(), }; this.clients.update_geometry(&this.conn)?; keys::grab(&mut this.conn)?; Ok(this) } /// Run the main event loop until we encounter a non-recoverable error (usually connection). /// This will only ever return an error. pub fn event_loop(&mut self) -> Result<()> { loop { match self.conn.wait_for_event() { Ok(e) => { self.dispatch_event(e)?; } Err(Error::Xcb(xcb::Error::Protocol(e))) => { eprintln!("protocol error in event loop: {e:#?}\ncontinuing anyway"); } Err(e) => { eprintln!("unrecoverable error: {e:#?}\nexiting event loop"); return Err(e); } }; self.conn.flush()?; } } pub fn dispatch_event(&mut self, e: xcb::Event) -> Result<()> { debug!("received event: {e:?}"); match e { // 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 buttons.rs Event::X(x::Event::ButtonPress(e)) => self.handle_button_press(&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), Event::X(x::Event::ClientMessage(e)) => self.handle_client_message(&e), _ => {} } Ok(()) } /// Update client properties when they change in X11. fn handle_property_notify(&mut self, e: &PropertyNotifyEvent) { if x::ATOM_WM_HINTS == e.atom() { let focused = self.clients.is_focused(e.window()); if let Some(c) = self.clients.find_client_mut(e.window()) { c.sync_hints(&self.conn, focused); } } } /// Handle some common client requests set out by the EWMH spec fn handle_client_message(&mut self, e: &ClientMessageEvent) { let Some((mon, pos)) = self.clients.find_client_pos(e.window()) else { return; }; if e.format() != 32 { return; } if e.r#type() == self.conn.atoms.net_wm_state { let x::ClientMessageData::Data32(data) = e.data() else { unreachable!(); }; if !(data[1] == self.conn.atoms.net_wm_fullscreen.resource_id() || data[2] == self.conn.atoms.net_wm_fullscreen.resource_id()) { return; } let mon_geom = self.clients.mon_geometry(mon); let c = self.clients.client_mut(mon, pos).unwrap(); let fullscreen = match data[0] { 1 => true, 2 => !c.fullscreen(), _ => false, }; if fullscreen { c.set_fullscreen(&self.conn, &mon_geom); } else { c.set_tiled(&self.conn); } self.clients.rearrange(&self.conn); } } } /// Cleanup this process' children and set some flags. /// This is necessary when used with `startx`. fn cleanup_process_children() { unsafe { // Don't transform children into zombies when they terminate sigaction( Signal::SIGCHLD, &SigAction::new( SigHandler::SigIgn, SaFlags::SA_NOCLDSTOP | SaFlags::SA_NOCLDWAIT | SaFlags::SA_RESTART, SigSet::empty(), ), ) .unwrap(); // Immediately wait for zombie processes to die - sometimes these come from startx. while waitpid(Pid::from_raw(-1), Some(WaitPidFlag::WNOHANG)).is_ok() {} }; }