Skip to main content

xlib_display_server/
xwrap.rs

1//! A wrapper around calls to xlib and X related functions.
2// We allow this _ because if we don't we'll receive an error that it isn't read on _task_guard.
3#![allow(clippy::used_underscore_binding)]
4// We allow this so that extern "C" functions are not flagged as confusing. The current placement
5// allows for easy reading.
6#![allow(clippy::items_after_statements)]
7// We allow this because _y_ and _x_ are intentionally similar. Changing it makes the code noisy.
8#![allow(clippy::similar_names)]
9use crate::XlibWindowHandle;
10
11use super::xatom::XAtom;
12use super::xcursor::XCursor;
13use super::{Screen, Window, WindowHandle, utils};
14use leftwm_core::config::{Config, WindowHidingStrategy};
15use leftwm_core::models::{FocusBehaviour, FocusOnActivationBehaviour, Mode};
16use leftwm_core::utils::modmask_lookup::ModMask;
17use std::ffi::CString;
18use std::os::raw::{c_char, c_double, c_int, c_long, c_short, c_ulong};
19use std::sync::Arc;
20use std::{ptr, slice};
21use tokio::sync::{Notify, oneshot};
22use tokio::time::Duration;
23
24use x11_dl::xlib;
25use x11_dl::xrandr::Xrandr;
26
27mod getters;
28mod mouse;
29mod setters;
30mod window;
31
32type WindowStateConst = c_long;
33pub const WITHDRAWN_STATE: WindowStateConst = 0;
34pub const NORMAL_STATE: WindowStateConst = 1;
35pub const ICONIC_STATE: WindowStateConst = 2;
36const MAX_PROPERTY_VALUE_LEN: c_long = 4096;
37
38pub const ROOT_EVENT_MASK: c_long = xlib::SubstructureRedirectMask
39    | xlib::SubstructureNotifyMask
40    | xlib::ButtonPressMask
41    | xlib::PointerMotionMask
42    | xlib::StructureNotifyMask;
43
44const BUTTONMASK: c_long = xlib::ButtonPressMask | xlib::ButtonReleaseMask | xlib::ButtonMotionMask;
45const MOUSEMASK: c_long = BUTTONMASK | xlib::PointerMotionMask;
46
47const X_CONFIGUREWINDOW: u8 = 12;
48const X_GRABBUTTON: u8 = 28;
49const X_GRABKEY: u8 = 33;
50const X_SETINPUTFOCUS: u8 = 42;
51const X_COPYAREA: u8 = 62;
52const X_POLYSEGMENT: u8 = 66;
53const X_POLYFILLRECTANGLE: u8 = 70;
54const X_POLYTEXT8: u8 = 74;
55
56const extern "C" fn on_error_from_xlib(_: *mut xlib::Display, er: *mut xlib::XErrorEvent) -> c_int {
57    let err = unsafe { *er };
58    let ec = err.error_code;
59    let rc = err.request_code;
60    let ba = ec == xlib::BadAccess;
61    let bd = ec == xlib::BadDrawable;
62    let bm = ec == xlib::BadMatch;
63
64    if ec == xlib::BadWindow
65        || (rc == X_CONFIGUREWINDOW && bm)
66        || (rc == X_GRABBUTTON && ba)
67        || (rc == X_GRABKEY && ba)
68        || (rc == X_SETINPUTFOCUS && bm)
69        || (rc == X_COPYAREA && bd)
70        || (rc == X_POLYSEGMENT && bd)
71        || (rc == X_POLYFILLRECTANGLE && bd)
72        || (rc == X_POLYTEXT8 && bd)
73    {
74        return 0;
75    }
76    1
77}
78
79pub extern "C" fn on_error_from_xlib_dummy(
80    _: *mut xlib::Display,
81    _: *mut xlib::XErrorEvent,
82) -> c_int {
83    1
84}
85
86pub struct Colors {
87    normal: c_ulong,
88    floating: c_ulong,
89    active: c_ulong,
90    background: c_ulong,
91}
92
93#[derive(Debug, Clone)]
94pub enum XlibError {
95    FailedStatus,
96    RootWindowNotFound,
97    InvalidXAtom,
98}
99
100/// Contains Xserver information and origins.
101pub struct XWrap {
102    xlib: xlib::Xlib,
103    display: *mut xlib::Display,
104    root: xlib::Window,
105    pub atoms: XAtom,
106    cursors: XCursor,
107    colors: Colors,
108    pub managed_windows: Vec<xlib::Window>,
109    pub focused_window: xlib::Window,
110    pub tag_labels: Vec<String>,
111    pub mode: Mode<XlibWindowHandle>,
112    pub focus_behaviour: FocusBehaviour,
113    pub focus_on_activation: FocusOnActivationBehaviour,
114    pub mouse_key_mask: ModMask,
115    pub mode_origin: (i32, i32),
116    _task_guard: oneshot::Receiver<()>,
117    pub task_notify: Arc<Notify>,
118    pub motion_event_limiter: c_ulong,
119    pub refresh_rate: c_short,
120    pub window_hiding_strategy: WindowHidingStrategy,
121}
122
123impl Default for XWrap {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl XWrap {
130    /// # Panics
131    ///
132    /// Panics if unable to contact xorg.
133    // TODO: Split this function up.
134    // `XOpenDisplay`: https://tronche.com/gui/x/xlib/display/opening.html
135    // `XConnectionNumber`: https://tronche.com/gui/x/xlib/display/display-macros.html#ConnectionNumber
136    // `XDefaultRootWindow`: https://tronche.com/gui/x/xlib/display/display-macros.html#DefaultRootWindow
137    // `XSetErrorHandler`: https://tronche.com/gui/x/xlib/event-handling/protocol-errors/XSetErrorHandler.html
138    // `XSelectInput`: https://tronche.com/gui/x/xlib/event-handling/XSelectInput.html
139    #[must_use]
140    #[allow(clippy::too_many_lines)]
141    pub fn new() -> Self {
142        const SERVER: mio::Token = mio::Token(0);
143        let xlib = xlib::Xlib::open().expect("Couldn't not connect to Xorg Server");
144        let display = unsafe { (xlib.XOpenDisplay)(ptr::null()) };
145        assert!(!display.is_null(), "Null pointer in display");
146
147        let fd = unsafe { (xlib.XConnectionNumber)(display) };
148
149        let (guard, _task_guard) = oneshot::channel();
150        let notify = Arc::new(Notify::new());
151        let task_notify = notify.clone();
152
153        let mut poll = mio::Poll::new().expect("Unable to boot Mio");
154        let mut events = mio::Events::with_capacity(1);
155        poll.registry()
156            .register(
157                &mut mio::unix::SourceFd(&fd),
158                SERVER,
159                mio::Interest::READABLE,
160            )
161            .expect("Unable to boot Mio");
162        let timeout = Duration::from_millis(100);
163        tokio::task::spawn_blocking(move || {
164            loop {
165                if guard.is_closed() {
166                    return;
167                }
168
169                if let Err(err) = poll.poll(&mut events, Some(timeout)) {
170                    tracing::warn!("Xlib socket poll failed with {:?}", err);
171                    continue;
172                }
173
174                events
175                    .iter()
176                    .filter(|event| SERVER == event.token())
177                    .for_each(|_| notify.notify_one());
178            }
179        });
180
181        let atoms = XAtom::new(&xlib, display);
182        let cursors = XCursor::new(&xlib, display);
183        let root = unsafe { (xlib.XDefaultRootWindow)(display) };
184
185        let colors = Colors {
186            normal: 0,
187            floating: 0,
188            active: 0,
189            background: 0,
190        };
191
192        let refresh_rate = match Xrandr::open() {
193            // Get the current refresh rate from xrandr if available.
194            Ok(xrandr) => unsafe {
195                let screen_resources = (xrandr.XRRGetScreenResources)(display, root);
196                let crtcs = slice::from_raw_parts(
197                    (*screen_resources).crtcs,
198                    (*screen_resources).ncrtc as usize,
199                );
200                let active_modes: Vec<c_ulong> = crtcs
201                    .iter()
202                    .map(|crtc| (xrandr.XRRGetCrtcInfo)(display, screen_resources, *crtc))
203                    .filter(|&crtc_info| (*crtc_info).mode != 0)
204                    .map(|crtc_info| (*crtc_info).mode)
205                    .collect();
206                let modes = slice::from_raw_parts(
207                    (*screen_resources).modes,
208                    (*screen_resources).nmode as usize,
209                );
210                modes
211                    .iter()
212                    .filter(|mode_info| active_modes.contains(&mode_info.id))
213                    .map(|mode_info| {
214                        (mode_info.dotClock as c_double
215                            / c_double::from(mode_info.hTotal * mode_info.vTotal))
216                            as c_short
217                    })
218                    .max()
219                    .unwrap_or(60)
220            },
221            Err(_) => 60,
222        };
223
224        tracing::debug!("Refresh Rate: {}", refresh_rate);
225
226        let xw = Self {
227            xlib,
228            display,
229            root,
230            atoms,
231            cursors,
232            colors,
233            managed_windows: vec![],
234            focused_window: root,
235            tag_labels: vec![],
236            mode: Mode::Normal,
237            focus_behaviour: FocusBehaviour::Sloppy,
238            focus_on_activation: FocusOnActivationBehaviour::MarkUrgent,
239            mouse_key_mask: ModMask::Zero,
240            mode_origin: (0, 0),
241            _task_guard,
242            task_notify,
243            motion_event_limiter: 0,
244            refresh_rate,
245            window_hiding_strategy: WindowHidingStrategy::default(),
246        };
247
248        // Check that another WM is not running.
249        extern "C" fn startup_check_for_other_wm(
250            _: *mut xlib::Display,
251            _: *mut xlib::XErrorEvent,
252        ) -> c_int {
253            tracing::error!("ERROR: another window manager is already running");
254            ::std::process::exit(-1);
255        }
256        unsafe {
257            (xw.xlib.XSetErrorHandler)(Some(startup_check_for_other_wm));
258            (xw.xlib.XSelectInput)(xw.display, root, xlib::SubstructureRedirectMask);
259        };
260        xw.sync();
261
262        unsafe { (xw.xlib.XSetErrorHandler)(Some(on_error_from_xlib)) };
263        xw.sync();
264        xw
265    }
266
267    pub fn load_config(&mut self, config: &impl Config) {
268        self.focus_behaviour = config.focus_behaviour();
269        self.focus_on_activation = config.focus_on_activation();
270        self.mouse_key_mask = utils::modmask_lookup::into_modmask(&config.mousekey());
271        self.tag_labels = config.create_list_of_tag_labels();
272        self.colors = Colors {
273            normal: self.get_color(config.default_border_color()),
274            floating: self.get_color(config.floating_border_color()),
275            active: self.get_color(config.focused_border_color()),
276            background: self.get_color(config.background_color()),
277        };
278        self.window_hiding_strategy = config.window_hiding_strategy();
279    }
280
281    /// Initialize the xwrapper.
282    // `XChangeWindowAttributes`: https://tronche.com/gui/x/xlib/window/XChangeWindowAttributes.html
283    // `XDeleteProperty`: https://tronche.com/gui/x/xlib/window-information/XDeleteProperty.html
284    // TODO: split into smaller functions
285    pub fn init(&mut self) {
286        let root = self.root;
287
288        let mut attrs: xlib::XSetWindowAttributes = unsafe { std::mem::zeroed() };
289        attrs.cursor = self.cursors.normal;
290        attrs.event_mask = ROOT_EVENT_MASK;
291
292        unsafe {
293            (self.xlib.XChangeWindowAttributes)(
294                self.display,
295                self.root,
296                xlib::CWEventMask | xlib::CWCursor,
297                &raw mut attrs,
298            );
299        }
300
301        self.subscribe_to_event(root, ROOT_EVENT_MASK);
302
303        // EWMH compliance.
304        unsafe {
305            let supported: Vec<c_long> = self
306                .atoms
307                .net_supported()
308                .iter()
309                .map(|&atom| atom as c_long)
310                .collect();
311            self.replace_property_long(root, self.atoms.NetSupported, xlib::XA_ATOM, &supported);
312            std::mem::forget(supported);
313            // Cleanup the client list.
314            (self.xlib.XDeleteProperty)(self.display, root, self.atoms.NetClientList);
315        }
316
317        // EWMH compliance for desktops.
318        self.init_desktops_hints();
319
320        self.sync();
321    }
322
323    /// EWMH support used for bars such as polybar.
324    ///  # Panics
325    ///
326    ///  Panics if a new Cstring cannot be formed
327    // `Xutf8TextListToTextProperty`: https://linux.die.net/man/3/xutf8textlisttotextproperty
328    // `XSetTextProperty`: https://tronche.com/gui/x/xlib/ICC/client-to-window-manager/XSetTextProperty.html
329    pub fn init_desktops_hints(&self) {
330        let tag_labels = &self.tag_labels;
331        let tag_length = tag_labels.len();
332        // Set the number of desktop.
333        let data = vec![tag_length as u32];
334        self.set_desktop_prop(&data, self.atoms.NetNumberOfDesktops);
335        // Set a current desktop.
336        let data = vec![0_u32, xlib::CurrentTime as u32];
337        self.set_desktop_prop(&data, self.atoms.NetCurrentDesktop);
338        // Set desktop names.
339        let mut text: xlib::XTextProperty = unsafe { std::mem::zeroed() };
340        unsafe {
341            let mut clist_tags: Vec<*mut c_char> = tag_labels
342                .iter()
343                .map(|x| CString::new(x.clone()).unwrap_or_default().into_raw())
344                .collect();
345            let ptr = clist_tags.as_mut_ptr();
346            (self.xlib.Xutf8TextListToTextProperty)(
347                self.display,
348                ptr,
349                clist_tags.len() as i32,
350                xlib::XUTF8StringStyle,
351                &raw mut text,
352            );
353            std::mem::forget(clist_tags);
354            (self.xlib.XSetTextProperty)(
355                self.display,
356                self.root,
357                &raw mut text,
358                self.atoms.NetDesktopNames,
359            );
360        }
361
362        // Set the WM NAME.
363        self.set_desktop_prop_string("LeftWM", self.atoms.NetWMName, self.atoms.UTF8String);
364
365        self.set_desktop_prop_string("LeftWM", self.atoms.WMClass, xlib::XA_STRING);
366
367        self.set_desktop_prop_c_ulong(
368            self.root as c_ulong,
369            self.atoms.NetSupportingWmCheck,
370            xlib::XA_WINDOW,
371        );
372
373        // Set a viewport.
374        let data = vec![0_u32, 0_u32];
375        self.set_desktop_prop(&data, self.atoms.NetDesktopViewport);
376    }
377
378    /// Send a xevent atom for a window to X.
379    // `XSendEvent`: https://tronche.com/gui/x/xlib/event-handling/XSendEvent.html
380    fn send_xevent_atom(&self, window: xlib::Window, atom: xlib::Atom) -> bool {
381        if self.can_send_xevent_atom(window, atom) {
382            let mut msg: xlib::XClientMessageEvent = unsafe { std::mem::zeroed() };
383            msg.type_ = xlib::ClientMessage;
384            msg.window = window;
385            msg.message_type = self.atoms.WMProtocols;
386            msg.format = 32;
387            msg.data.set_long(0, atom as c_long);
388            msg.data.set_long(1, xlib::CurrentTime as c_long);
389            let mut ev: xlib::XEvent = msg.into();
390            self.send_xevent(window, 0, xlib::NoEventMask, &mut ev);
391            return true;
392        }
393        false
394    }
395
396    /// Send a xevent for a window to X.
397    // `XSendEvent`: https://tronche.com/gui/x/xlib/event-handling/XSendEvent.html
398    pub fn send_xevent(
399        &self,
400        window: xlib::Window,
401        propogate: i32,
402        mask: c_long,
403        event: &mut xlib::XEvent,
404    ) {
405        unsafe { (self.xlib.XSendEvent)(self.display, window, propogate, mask, event) };
406        self.sync();
407    }
408
409    /// Returns whether a window can recieve a xevent atom.
410    // `XGetWMProtocols`: https://tronche.com/gui/x/xlib/ICC/client-to-window-manager/XGetWMProtocols.html
411    fn can_send_xevent_atom(&self, window: xlib::Window, atom: xlib::Atom) -> bool {
412        unsafe {
413            let mut array: *mut xlib::Atom = std::mem::zeroed();
414            let mut length: c_int = std::mem::zeroed();
415            let status: xlib::Status =
416                (self.xlib.XGetWMProtocols)(self.display, window, &raw mut array, &raw mut length);
417            let protocols: &[xlib::Atom] = slice::from_raw_parts(array, length as usize);
418            status > 0 && protocols.contains(&atom)
419        }
420    }
421
422    /// Update all the windows with the new colors.
423    pub fn update_colors(
424        &mut self,
425        focused: Option<WindowHandle<XlibWindowHandle>>,
426        windows: &[Window<XlibWindowHandle>],
427    ) {
428        for window in windows {
429            let WindowHandle(XlibWindowHandle(handle)) = window.handle;
430            let color: c_ulong = if focused == Some(window.handle) {
431                self.colors.active
432            } else if window.floating() {
433                self.colors.floating
434            } else {
435                self.colors.normal
436            };
437            self.set_window_border_color(handle, color);
438        }
439        self.set_background_color(self.colors.background);
440    }
441
442    /// Sets the mode within our xwrapper.
443    pub fn set_mode(&mut self, mode: Mode<XlibWindowHandle>) {
444        match mode {
445            // Prevent resizing and moving of root.
446            Mode::MovingWindow(h)
447            | Mode::ResizingWindow(h)
448            | Mode::ReadyToMove(h)
449            | Mode::ReadyToResize(h)
450                if h == self.get_default_root_handle() => {}
451            Mode::ReadyToMove(_) | Mode::ReadyToResize(_) if self.mode == Mode::Normal => {
452                self.mode = mode;
453                if let Ok(loc) = self.get_cursor_point() {
454                    self.mode_origin = loc;
455                }
456                let cursor = match mode {
457                    Mode::ReadyToResize(_) | Mode::ResizingWindow(_) => self.cursors.resize,
458                    Mode::ReadyToMove(_) | Mode::MovingWindow(_) => self.cursors.move_,
459                    Mode::Normal => self.cursors.normal,
460                };
461                self.grab_pointer(cursor);
462            }
463            Mode::MovingWindow(h) | Mode::ResizingWindow(h)
464                if self.mode == Mode::ReadyToMove(h) || self.mode == Mode::ReadyToResize(h) =>
465            {
466                self.ungrab_pointer();
467                self.mode = mode;
468                let cursor = match mode {
469                    Mode::ReadyToResize(_) | Mode::ResizingWindow(_) => self.cursors.resize,
470                    Mode::ReadyToMove(_) | Mode::MovingWindow(_) => self.cursors.move_,
471                    Mode::Normal => self.cursors.normal,
472                };
473                self.grab_pointer(cursor);
474            }
475            Mode::Normal => {
476                self.ungrab_pointer();
477                self.mode = mode;
478            }
479            _ => {}
480        }
481    }
482
483    /// Wait until readable.
484    pub async fn wait_readable(&mut self) {
485        self.task_notify.notified().await;
486    }
487
488    /// Flush and sync the xserver.
489    // `XSync`: https://tronche.com/gui/x/xlib/event-handling/XSync.html
490    pub fn sync(&self) {
491        unsafe { (self.xlib.XSync)(self.display, xlib::False) };
492    }
493
494    /// Flush the xserver.
495    // `XFlush`: https://tronche.com/gui/x/xlib/event-handling/XFlush.html
496    pub fn flush(&self) {
497        unsafe { (self.xlib.XFlush)(self.display) };
498    }
499
500    /// Returns how many events are waiting.
501    // `XPending`: https://tronche.com/gui/x/xlib/event-handling/XPending.html
502    #[must_use]
503    pub fn queue_len(&self) -> i32 {
504        unsafe { (self.xlib.XPending)(self.display) }
505    }
506}