this repo has no description
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}