this repo has no description
at main 214 lines 7.1 kB view raw
1//! A lightweight X11 window manager, inspired by dwm. 2//! 3//! # Structure 4//! 5//! 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. 6//! 7//! [`conn_info`] wraps XCB's [`xcb::Connection`] type and caches common resources such as atoms, colours, cursors, and keyboard layout info. 8//! 9//! `focus.rs`, [`keys`], and [`clients`] all add some event handlers to [`WM`], but most of the important code is in [`clients`]. 10//! 11//! [`config`] holds all of the variables that a user might want to change. 12//! 13//! # XCB 14//! 15//! Unlike dwm, blow uses XCB rather than Xlib. This means requests are asynchronous by default. 16//! In most places, we avoid checking for errors unless we need to see the response to a request. 17//! Errors will be caught and logged in the event loop instead. See [`xcb`] documentation for more details. 18#![deny(clippy::all, clippy::pedantic, clippy::nursery)] 19#![allow(clippy::must_use_candidate, clippy::missing_errors_doc)] 20 21use clients::ClientState; 22use conn_info::Connection; 23pub use error::*; 24use nix::{ 25 sys::{ 26 signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal}, 27 wait::{waitpid, WaitPidFlag}, 28 }, 29 unistd::Pid, 30}; 31use xcb::{ 32 x::{self, ClientMessageEvent, PropertyNotifyEvent}, 33 Connection as RawConnection, Event, Extension, Xid, 34}; 35 36pub mod buttons; 37pub mod clients; 38pub mod config; 39pub mod conn_info; 40#[doc(hidden)] 41mod error; 42#[doc(hidden)] 43mod focus; 44pub mod helpers; 45pub mod keys; 46pub mod log; 47 48/// Do the thing! 49fn main() -> Result<()> { 50 cleanup_process_children(); 51 52 let (conn, screen_num) = 53 RawConnection::connect_with_extensions(None, &[], &[Extension::Xinerama])?; 54 55 #[allow(clippy::cast_sign_loss)] 56 let mut wm = WM::new(&conn, screen_num as usize)?; 57 58 #[cfg(feature = "autostart")] 59 { 60 for to_start in config::AUTOSTART_SCRIPTS { 61 helpers::spawn(to_start[0], &to_start[1..]); 62 } 63 } 64 65 wm.event_loop()?; 66 67 Ok(()) 68} 69 70/// All of the state used by the window manager 71pub struct WM<'a> { 72 conn: Connection<'a>, 73 clients: ClientState, 74} 75 76impl<'a> WM<'a> { 77 /// Prepare to start the window manager, using the given connection and scren number. 78 pub fn new(conn: &'a RawConnection, screen_num: usize) -> Result<Self> { 79 let mut this = Self { 80 conn: Connection::new(conn, screen_num)?, 81 clients: ClientState::default(), 82 }; 83 84 this.clients.update_geometry(&this.conn)?; 85 keys::grab(&mut this.conn)?; 86 87 Ok(this) 88 } 89 90 /// Run the main event loop until we encounter a non-recoverable error (usually connection). 91 /// This will only ever return an error. 92 pub fn event_loop(&mut self) -> Result<()> { 93 loop { 94 match self.conn.wait_for_event() { 95 Ok(e) => { 96 if let Err(err) = self.dispatch_event(e) { 97 eprintln!("error when handling event: {err:#?}\ncontinuing anyway"); 98 } 99 } 100 Err(Error::Xcb(xcb::Error::Protocol(e))) => { 101 eprintln!("protocol error in event loop: {e:#?}\ncontinuing anyway"); 102 } 103 Err(e) => { 104 eprintln!("unrecoverable error: {e:#?}\nexiting event loop"); 105 return Err(e); 106 } 107 }; 108 self.conn.flush()?; 109 } 110 } 111 112 pub fn dispatch_event(&mut self, e: xcb::Event) -> Result<()> { 113 debug!("received event: {e:?}"); 114 115 match e { 116 // See keys.rs 117 Event::X(x::Event::KeyPress(e)) => self.handle_key_press(&e), 118 Event::X(x::Event::MappingNotify(e)) => self.handle_mapping_notify(&e)?, 119 120 // See buttons.rs 121 Event::X(x::Event::ButtonPress(e)) => self.handle_button_press(&e), 122 123 // See clients/mod.rs 124 Event::X(x::Event::ConfigureRequest(e)) => { 125 self.handle_configure_request(&e); 126 } 127 Event::X(x::Event::ConfigureNotify(e)) => { 128 self.handle_configure_notify(&e)?; 129 } 130 Event::X(x::Event::DestroyNotify(e)) => self.handle_destroy_notify(&e), 131 Event::X(x::Event::MapRequest(e)) => self.handle_map_request(&e)?, 132 Event::X(x::Event::UnmapNotify(e)) => self.handle_unmap_notify(&e), 133 134 // See focus.rs 135 Event::X(x::Event::EnterNotify(e)) => self.handle_enter_notify(&e), 136 Event::X(x::Event::FocusIn(e)) => self.handle_focus_in(&e), 137 138 // See below 139 Event::X(x::Event::PropertyNotify(e)) => self.handle_property_notify(&e), 140 Event::X(x::Event::ClientMessage(e)) => self.handle_client_message(&e), 141 _ => {} 142 } 143 144 Ok(()) 145 } 146 147 /// Update client properties when they change in X11. 148 fn handle_property_notify(&mut self, e: &PropertyNotifyEvent) { 149 if x::ATOM_WM_HINTS == e.atom() { 150 let focused = self.clients.is_focused(e.window()); 151 if let Some(c) = self.clients.find_client_mut(e.window()) { 152 c.sync_hints(&self.conn, focused); 153 } 154 } 155 } 156 157 /// Handle some common client requests set out by the EWMH spec 158 fn handle_client_message(&mut self, e: &ClientMessageEvent) { 159 let Some(pos) = self.clients.find_client_pos(e.window()) else { 160 return; 161 }; 162 163 if e.format() != 32 { 164 return; 165 } 166 167 if e.r#type() == self.conn.atoms.net_wm_state { 168 let x::ClientMessageData::Data32(data) = e.data() else { 169 unreachable!(); 170 }; 171 172 if !(data[1] == self.conn.atoms.net_wm_fullscreen.resource_id() 173 || data[2] == self.conn.atoms.net_wm_fullscreen.resource_id()) 174 { 175 return; 176 } 177 178 let mon_geom = self.clients.client_mon(pos).screen_info; 179 let c = self.clients.client_mut(pos); 180 let fullscreen = match data[0] { 181 1 => true, 182 2 => !c.fullscreen(), 183 _ => false, 184 }; 185 186 if fullscreen { 187 c.set_fullscreen(&self.conn, &mon_geom); 188 } else { 189 c.set_tiled(&self.conn); 190 } 191 self.clients.rearrange(&self.conn); 192 } 193 } 194} 195 196/// Cleanup this process' children and set some flags. 197/// This is necessary when used with `startx`. 198fn cleanup_process_children() { 199 unsafe { 200 // Don't transform children into zombies when they terminate 201 sigaction( 202 Signal::SIGCHLD, 203 &SigAction::new( 204 SigHandler::SigIgn, 205 SaFlags::SA_NOCLDSTOP | SaFlags::SA_NOCLDWAIT | SaFlags::SA_RESTART, 206 SigSet::empty(), 207 ), 208 ) 209 .unwrap(); 210 211 // Immediately wait for zombie processes to die - sometimes these come from startx. 212 while waitpid(Pid::from_raw(-1), Some(WaitPidFlag::WNOHANG)).is_ok() {} 213 }; 214}