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