// A very simple reparenting window manager. // This WM does NOT follow ICCCM! extern crate x11rb; use std::cmp::Reverse; use std::collections::{BinaryHeap, HashSet}; use std::process::exit; use x11rb::connection::Connection; use x11rb::errors::{ReplyError, ReplyOrIdError}; use x11rb::protocol::xproto::*; use x11rb::protocol::{ErrorKind, Event}; use x11rb::{COPY_DEPTH_FROM_PARENT, CURRENT_TIME}; const TITLEBAR_HEIGHT: u16 = 20; const DRAG_BUTTON: Button = 1; /// The state of a single window that we manage #[derive(Debug)] struct WindowState { window: Window, frame_window: Window, x: i16, y: i16, width: u16, } impl WindowState { fn new(window: Window, frame_window: Window, geom: &GetGeometryReply) -> WindowState { WindowState { window, frame_window, x: geom.x, y: geom.y, width: geom.width, } } fn close_x_position(&self) -> i16 { std::cmp::max(0, self.width - TITLEBAR_HEIGHT) as _ } } /// The state of the full WM #[derive(Debug)] struct WmState<'a, C: Connection> { conn: &'a C, screen_num: usize, black_gc: Gcontext, windows: Vec, pending_expose: HashSet, wm_protocols: Atom, wm_delete_window: Atom, sequences_to_ignore: BinaryHeap>, // If this is Some, we are currently dragging the given window with the given offset relative // to the mouse. drag_window: Option<(Window, (i16, i16))>, } impl<'a, C: Connection> WmState<'a, C> { fn new(conn: &'a C, screen_num: usize) -> Result, ReplyOrIdError> { let screen = &conn.setup().roots[screen_num]; let black_gc = conn.generate_id()?; let font = conn.generate_id()?; conn.open_font(font, b"9x15")?; let gc_aux = CreateGCAux::new() .graphics_exposures(0) .background(screen.white_pixel) .foreground(screen.black_pixel) .font(font); conn.create_gc(black_gc, screen.root, &gc_aux)?; conn.close_font(font)?; let wm_protocols = conn.intern_atom(false, b"WM_PROTOCOLS")?; let wm_delete_window = conn.intern_atom(false, b"WM_DELETE_WINDOW")?; Ok(WmState { conn, screen_num, black_gc, windows: Vec::default(), pending_expose: HashSet::default(), wm_protocols: wm_protocols.reply()?.atom, wm_delete_window: wm_delete_window.reply()?.atom, sequences_to_ignore: Default::default(), drag_window: None, }) } /// Scan for already existing windows and manage them fn scan_windows(&mut self) -> Result<(), ReplyOrIdError> { // Get the already existing top-level windows. let screen = &self.conn.setup().roots[self.screen_num]; let tree_reply = self.conn.query_tree(screen.root)?.reply()?; // For each window, request its attributes and geometry *now* let mut cookies = Vec::with_capacity(tree_reply.children.len()); for win in tree_reply.children { let attr = self.conn.get_window_attributes(win)?; let geom = self.conn.get_geometry(win)?; cookies.push((win, attr, geom)); } // Get the replies and manage windows for (win, attr, geom) in cookies { if let (Ok(attr), Ok(geom)) = (attr.reply(), geom.reply()) { if !attr.override_redirect && attr.map_state != MapState::UNMAPPED { self.manage_window(win, &geom)?; } } else { // Just skip this window } } Ok(()) } /// Add a new window that should be managed by the WM fn manage_window( &mut self, win: Window, geom: &GetGeometryReply, ) -> Result<(), ReplyOrIdError> { println!("Managing window {win:?}"); let screen = &self.conn.setup().roots[self.screen_num]; assert!(self.find_window_by_id(win).is_none()); let frame_win = self.conn.generate_id()?; let win_aux = CreateWindowAux::new() .event_mask( EventMask::EXPOSURE | EventMask::SUBSTRUCTURE_NOTIFY | EventMask::BUTTON_PRESS | EventMask::BUTTON_RELEASE | EventMask::POINTER_MOTION | EventMask::ENTER_WINDOW, ) .background_pixel(screen.white_pixel); self.conn.create_window( COPY_DEPTH_FROM_PARENT, frame_win, screen.root, geom.x, geom.y, geom.width, geom.height + TITLEBAR_HEIGHT, 1, WindowClass::INPUT_OUTPUT, 0, &win_aux, )?; self.conn.grab_server()?; self.conn.change_save_set(SetMode::INSERT, win)?; let cookie = self .conn .reparent_window(win, frame_win, 0, TITLEBAR_HEIGHT as _)?; self.conn.map_window(win)?; self.conn.map_window(frame_win)?; self.conn.ungrab_server()?; self.windows.push(WindowState::new(win, frame_win, geom)); // Ignore all events caused by reparent_window(). All those events have the sequence number // of the reparent_window() request, thus remember its sequence number. The // grab_server()/ungrab_server() is done so that the server does not handle other clients // in-between, which could cause other events to get the same sequence number. self.sequences_to_ignore .push(Reverse(cookie.sequence_number() as u16)); Ok(()) } /// Draw the titlebar of a window fn redraw_titlebar(&self, state: &WindowState) -> Result<(), ReplyError> { let close_x = state.close_x_position(); self.conn.poly_line( CoordMode::ORIGIN, state.frame_window, self.black_gc, &[ Point { x: close_x, y: 0 }, Point { x: state.width as _, y: TITLEBAR_HEIGHT as _, }, ], )?; self.conn.poly_line( CoordMode::ORIGIN, state.frame_window, self.black_gc, &[ Point { x: close_x, y: TITLEBAR_HEIGHT as _, }, Point { x: state.width as _, y: 0, }, ], )?; let reply = self .conn .get_property( false, state.window, AtomEnum::WM_NAME, AtomEnum::STRING, 0, u32::MAX, )? .reply()?; self.conn .image_text8(state.frame_window, self.black_gc, 1, 10, &reply.value)?; Ok(()) } /// Do all pending work that was queued while handling some events fn refresh(&mut self) { while let Some(&win) = self.pending_expose.iter().next() { self.pending_expose.remove(&win); if let Some(state) = self.find_window_by_id(win) { if let Err(err) = self.redraw_titlebar(state) { eprintln!( "Error while redrawing window {:x?}: {:?}", state.window, err ); } } } } fn find_window_by_id(&self, win: Window) -> Option<&WindowState> { self.windows .iter() .find(|state| state.window == win || state.frame_window == win) } fn find_window_by_id_mut(&mut self, win: Window) -> Option<&mut WindowState> { self.windows .iter_mut() .find(|state| state.window == win || state.frame_window == win) } /// Handle the given event fn handle_event(&mut self, event: Event) -> Result<(), ReplyOrIdError> { let mut should_ignore = false; if let Some(seqno) = event.wire_sequence_number() { // Check sequences_to_ignore and remove entries with old (=smaller) numbers. while let Some(&Reverse(to_ignore)) = self.sequences_to_ignore.peek() { // Sequence numbers can wrap around, so we cannot simply check for // "to_ignore <= seqno". This is equivalent to "to_ignore - seqno <= 0", which is what we // check instead. Since sequence numbers are unsigned, we need a trick: We decide // that values from [MAX/2, MAX] count as "<= 0" and the rest doesn't. if to_ignore.wrapping_sub(seqno) <= u16::MAX / 2 { // If the two sequence numbers are equal, this event should be ignored. should_ignore = to_ignore == seqno; break; } self.sequences_to_ignore.pop(); } } println!("Got event {event:?}"); if should_ignore { println!(" [ignored]"); return Ok(()); } match event { Event::UnmapNotify(event) => self.handle_unmap_notify(event), Event::ConfigureRequest(event) => self.handle_configure_request(event)?, Event::MapRequest(event) => self.handle_map_request(event)?, Event::Expose(event) => self.handle_expose(event), Event::EnterNotify(event) => self.handle_enter(event)?, Event::ButtonPress(event) => self.handle_button_press(event), Event::ButtonRelease(event) => self.handle_button_release(event)?, Event::MotionNotify(event) => self.handle_motion_notify(event)?, _ => {} } Ok(()) } fn handle_unmap_notify(&mut self, event: UnmapNotifyEvent) { let root = self.conn.setup().roots[self.screen_num].root; let conn = self.conn; self.windows.retain(|state| { if state.window != event.window { return true; } conn.change_save_set(SetMode::DELETE, state.window).unwrap(); conn.reparent_window(state.window, root, state.x, state.y) .unwrap(); conn.destroy_window(state.frame_window).unwrap(); false }); } fn handle_configure_request(&mut self, event: ConfigureRequestEvent) -> Result<(), ReplyError> { if let Some(state) = self.find_window_by_id_mut(event.window) { let _ = state; unimplemented!(); } // Allow clients to change everything, except sibling / stack mode let aux = ConfigureWindowAux::from_configure_request(&event) .sibling(None) .stack_mode(None); println!("Configure: {aux:?}"); self.conn.configure_window(event.window, &aux)?; Ok(()) } fn handle_map_request(&mut self, event: MapRequestEvent) -> Result<(), ReplyOrIdError> { self.manage_window( event.window, &self.conn.get_geometry(event.window)?.reply()?, ) } fn handle_expose(&mut self, event: ExposeEvent) { self.pending_expose.insert(event.window); } fn handle_enter(&mut self, event: EnterNotifyEvent) -> Result<(), ReplyError> { if let Some(state) = self.find_window_by_id(event.event) { // Set the input focus (ignoring ICCCM's WM_PROTOCOLS / WM_TAKE_FOCUS) self.conn .set_input_focus(InputFocus::PARENT, state.window, CURRENT_TIME)?; // Also raise the window to the top of the stacking order self.conn.configure_window( state.frame_window, &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE), )?; } Ok(()) } fn handle_button_press(&mut self, event: ButtonPressEvent) { if event.detail != DRAG_BUTTON || u16::from(event.state) != 0 { return; } if let Some(state) = self.find_window_by_id(event.event) { if self.drag_window.is_none() && event.event_x < state.close_x_position() { let (x, y) = (-event.event_x, -event.event_y); self.drag_window = Some((state.frame_window, (x, y))); } } } fn handle_button_release(&mut self, event: ButtonReleaseEvent) -> Result<(), ReplyError> { if event.detail == DRAG_BUTTON { self.drag_window = None; } if let Some(state) = self.find_window_by_id(event.event) { if event.event_x >= state.close_x_position() { let event = ClientMessageEvent::new( 32, state.window, self.wm_protocols, [self.wm_delete_window, 0, 0, 0, 0], ); self.conn .send_event(false, state.window, EventMask::NO_EVENT, event)?; } } Ok(()) } fn handle_motion_notify(&mut self, event: MotionNotifyEvent) -> Result<(), ReplyError> { if let Some((win, (x, y))) = self.drag_window { let (x, y) = (x + event.root_x, y + event.root_y); // Sigh, X11 and its mixing up i16 and i32 let (x, y) = (x as i32, y as i32); self.conn .configure_window(win, &ConfigureWindowAux::new().x(x).y(y))?; } Ok(()) } } fn become_wm(conn: &C, screen: &Screen) -> Result<(), ReplyError> { // Try to become the window manager. This causes an error if there is already another WM. let change = ChangeWindowAttributesAux::default() .event_mask(EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY); let res = conn.change_window_attributes(screen.root, &change)?.check(); if let Err(ReplyError::X11Error(ref error)) = res { if error.error_kind == ErrorKind::Access { eprintln!("Another WM is already running."); exit(1); } else { res } } else { res } } fn main() -> Result<(), Box> { let (conn, screen_num) = connect(None)?; // The following is only needed for start_timeout_thread(), which is used for 'tests' let conn1 = std::sync::Arc::new(conn); let conn = &*conn1; let screen = &conn.setup().roots[screen_num]; become_wm(conn, screen)?; let mut wm_state = WmState::new(conn, screen_num)?; wm_state.scan_windows()?; util::start_timeout_thread(conn1.clone(), screen.root); loop { wm_state.refresh(); conn.flush()?; let event = conn.wait_for_event()?; let mut event_option = Some(event); while let Some(event) = event_option { if let Event::ClientMessage(_) = event { // This is start_timeout_thread() signaling us to close (most likely). return Ok(()); } wm_state.handle_event(event)?; event_option = conn.poll_for_event()?; } } } include!("integration_test_util/connect.rs"); include!("integration_test_util/util.rs");