summaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 946c0cad3787496ff137ed01fd88a2f83770f1cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//! A lightweight X11 window manager, inspired by dwm.
//!
//! # Structure
//!
//! 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.
//!
//! [`conn_info`] wraps XCB's [`xcb::Connection`] type and caches common resources such as atoms, colours, cursors, and keyboard layout info.
//!
//! `focus.rs`, [`keys`], and [`clients`] all add some event handlers to [`WM`], but most of the important code is in [`clients`].
//!
//! [`config`] holds all of the variables that a user might want to change.
//!
//! # XCB
//!
//! Unlike dwm, blow uses XCB rather than Xlib. This means requests are asynchronous by default.
//! In most places, we avoid checking for errors unless we need to see the response to a request.
//! Errors will be caught and logged in the event loop instead. See [`xcb`] documentation for more details.
#![deny(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::must_use_candidate, clippy::missing_errors_doc)]

use clients::ClientState;
use conn_info::Connection;
pub use error::*;
use nix::{
    sys::{
        signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal},
        wait::{waitpid, WaitPidFlag},
    },
    unistd::Pid,
};
use xcb::{
    x::{self, ClientMessageEvent, PropertyNotifyEvent},
    Connection as RawConnection, Event, Extension, Xid,
};

pub mod buttons;
pub mod clients;
pub mod config;
pub mod conn_info;
#[doc(hidden)]
mod error;
#[doc(hidden)]
mod focus;
pub mod helpers;
pub mod keys;
pub mod log;

fn main() -> Result<()> {
    cleanup_process_children();

    let (conn, screen_num) =
        RawConnection::connect_with_extensions(None, &[], &[Extension::Xinerama])?;

    #[allow(clippy::cast_sign_loss)]
    let mut wm = WM::new(&conn, screen_num as usize)?;
    wm.event_loop()?;

    Ok(())
}

/// All of the state used by the window manager
pub struct WM<'a> {
    conn: Connection<'a>,
    clients: ClientState,
}

impl<'a> WM<'a> {
    /// Prepare to start the window manager, using the given connection and scren number.
    pub fn new(conn: &'a RawConnection, screen_num: usize) -> Result<Self> {
        let mut this = Self {
            conn: Connection::new(conn, screen_num)?,
            clients: ClientState::default(),
        };

        this.clients.update_geometry(&this.conn)?;
        keys::grab(&mut this.conn)?;

        Ok(this)
    }

    /// Run the main event loop until we encounter a non-recoverable error (usually connection).
    /// This will only ever return an error.
    pub fn event_loop(&mut self) -> Result<()> {
        loop {
            match self.conn.wait_for_event() {
                Ok(x) => {
                    debug!("received event: {x:?}");

                    match x {
                        // See keys.rs
                        Event::X(x::Event::KeyPress(e)) => self.handle_key_press(&e),
                        Event::X(x::Event::MappingNotify(e)) => self.handle_mapping_notify(&e)?,

                        // See buttons.rs
                        Event::X(x::Event::ButtonPress(e)) => self.handle_button_press(&e),

                        // See clients/mod.rs
                        Event::X(x::Event::ConfigureRequest(e)) => {
                            self.handle_configure_request(&e)?;
                        }
                        Event::X(x::Event::ConfigureNotify(e)) => {
                            self.handle_configure_notify(&e)?;
                        }
                        Event::X(x::Event::DestroyNotify(e)) => self.handle_destroy_notify(&e),
                        Event::X(x::Event::MapRequest(e)) => self.handle_map_request(&e)?,
                        Event::X(x::Event::UnmapNotify(e)) => self.handle_unmap_notify(&e),

                        // See focus.rs
                        Event::X(x::Event::EnterNotify(e)) => self.handle_enter_notify(&e),
                        Event::X(x::Event::FocusIn(e)) => self.handle_focus_in(&e),

                        // See below
                        Event::X(x::Event::PropertyNotify(e)) => self.handle_property_notify(&e),
                        Event::X(x::Event::ClientMessage(e)) => self.handle_client_message(&e),
                        _ => {}
                    }
                }
                Err(Error::Xcb(xcb::Error::Protocol(e))) => {
                    eprintln!("protocol error in event loop: {e:#?}\ncontinuing anyway");
                }
                Err(e) => {
                    eprintln!("unrecoverable error: {e:#?}\nexiting event loop");
                    return Err(e);
                }
            };
            self.conn.flush()?;
        }
    }

    /// Update client properties when they change in X11.
    fn handle_property_notify(&mut self, e: &PropertyNotifyEvent) {
        if x::ATOM_WM_HINTS == e.atom() {
            let focused = self.clients.is_focused(e.window());
            if let Some(c) = self.clients.find_client_mut(e.window()) {
                c.sync_hints(&self.conn, focused);
            }
        }
    }

    /// Handle some common client requests set out by the EWMH spec
    fn handle_client_message(&mut self, e: &ClientMessageEvent) {
        let Some((mon, pos)) = self.clients.find_client_pos(e.window()) else {
            return;
        };

        if e.format() != 32 {
            return;
        }

        if e.r#type() == self.conn.atoms.net_wm_state {
            let x::ClientMessageData::Data32(data) = e.data() else {
                unreachable!();
            };

            if !(data[1] == self.conn.atoms.net_wm_fullscreen.resource_id()
                || data[2] == self.conn.atoms.net_wm_fullscreen.resource_id())
            {
                return;
            }

            let mon_geom = self.clients.mon_geometry(mon);
            let c = self.clients.client_mut(mon, pos).unwrap();
            let fullscreen = match data[0] {
                1 => true,
                2 => !c.fullscreen(),
                _ => false,
            };

            if fullscreen {
                c.set_fullscreen(&self.conn, &mon_geom);
            } else {
                c.set_tiled(&self.conn);
            }
            self.clients.rearrange(&self.conn);
        }
    }
}

/// Cleanup this process' children and set some flags.
/// This is necessary when used with `startx`.
fn cleanup_process_children() {
    unsafe {
        // Don't transform children into zombies when they terminate
        sigaction(
            Signal::SIGCHLD,
            &SigAction::new(
                SigHandler::SigIgn,
                SaFlags::SA_NOCLDSTOP | SaFlags::SA_NOCLDWAIT | SaFlags::SA_RESTART,
                SigSet::empty(),
            ),
        )
        .unwrap();

        // Immediately wait for zombie processes to die - sometimes these come from startx.
        while waitpid(Pid::from_raw(-1), Some(WaitPidFlag::WNOHANG)).is_ok() {}
    };
}