gardesk/garchomp / c44f0c4

Browse files

initial skeleton: workspace, x11 composite, event loop, IPC

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c44f0c46bfc9d16055d4ee78e2689641c4ec0c89
Tree
e79d9e8

15 changed files

StatusFile+-
A .gitignore 23 0
A Cargo.toml 35 0
A garchomp-ipc/Cargo.toml 10 0
A garchomp-ipc/src/lib.rs 65 0
A garchomp/Cargo.toml 19 0
A garchomp/src/compositor/mod.rs 344 0
A garchomp/src/compositor/window.rs 93 0
A garchomp/src/ipc/mod.rs 98 0
A garchomp/src/main.rs 165 0
A garchomp/src/x11/atoms.rs 63 0
A garchomp/src/x11/composite.rs 121 0
A garchomp/src/x11/connection.rs 131 0
A garchomp/src/x11/mod.rs 9 0
A garchompctl/Cargo.toml 12 0
A garchompctl/src/main.rs 98 0
.gitignoreadded
@@ -0,0 +1,23 @@
1
+# Build artifacts
2
+/target/
3
+**/*.rs.bk
4
+Cargo.lock
5
+
6
+# IDE
7
+.idea/
8
+.vscode/
9
+*.swp
10
+*.swo
11
+*~
12
+
13
+# Documentation (planning files)
14
+/docs/ROADMAP.md
15
+/docs/sprints/
16
+
17
+# Logs
18
+*.log
19
+/tmp/
20
+
21
+# OS
22
+.DS_Store
23
+Thumbs.db
Cargo.tomladded
@@ -0,0 +1,35 @@
1
+[workspace]
2
+resolver = "2"
3
+members = ["garchomp", "garchompctl", "garchomp-ipc"]
4
+
5
+[workspace.package]
6
+version = "0.1.0"
7
+edition = "2024"
8
+authors = ["gardesk contributors"]
9
+license = "MIT"
10
+repository = "https://github.com/gardesk/garchomp"
11
+
12
+[workspace.dependencies]
13
+# X11
14
+x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr", "composite", "damage", "xfixes", "shape", "render"] }
15
+
16
+# Async
17
+tokio = { version = "1", features = ["full", "signal"] }
18
+
19
+# Serialization
20
+serde = { version = "1", features = ["derive"] }
21
+serde_json = "1"
22
+
23
+# CLI
24
+clap = { version = "4", features = ["derive"] }
25
+
26
+# Logging
27
+tracing = "0.1"
28
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
29
+
30
+# Errors
31
+thiserror = "2"
32
+anyhow = "1"
33
+
34
+# Internal
35
+garchomp-ipc = { path = "garchomp-ipc" }
garchomp-ipc/Cargo.tomladded
@@ -0,0 +1,10 @@
1
+[package]
2
+name = "garchomp-ipc"
3
+description = "IPC types for garchomp compositor"
4
+version.workspace = true
5
+edition.workspace = true
6
+license.workspace = true
7
+
8
+[dependencies]
9
+serde = { workspace = true }
10
+serde_json = { workspace = true }
garchomp-ipc/src/lib.rsadded
@@ -0,0 +1,65 @@
1
+//! IPC protocol types for garchomp compositor.
2
+
3
+use serde::{Deserialize, Serialize};
4
+
5
+/// Request sent to garchomp.
6
+#[derive(Debug, Clone, Serialize, Deserialize)]
7
+#[serde(tag = "type", rename_all = "snake_case")]
8
+pub enum Request {
9
+    /// Reload configuration.
10
+    Reload,
11
+    /// Toggle an effect.
12
+    SetEffect { effect: String, enabled: bool },
13
+    /// Set blur strength (0-20).
14
+    SetBlurStrength { strength: u32 },
15
+    /// Get window information.
16
+    GetWindowInfo { window: u32 },
17
+    /// List all managed windows.
18
+    ListWindows,
19
+    /// Ping for health check.
20
+    Ping,
21
+}
22
+
23
+/// Response from garchomp.
24
+#[derive(Debug, Clone, Serialize, Deserialize)]
25
+#[serde(tag = "type", rename_all = "snake_case")]
26
+pub enum Response {
27
+    /// Success with no data.
28
+    Ok,
29
+    /// Error response.
30
+    Error { message: String },
31
+    /// Pong response to ping.
32
+    Pong,
33
+    /// Window information.
34
+    WindowInfo(WindowInfo),
35
+    /// List of windows.
36
+    WindowList { windows: Vec<WindowInfo> },
37
+}
38
+
39
+/// Information about a managed window.
40
+#[derive(Debug, Clone, Serialize, Deserialize)]
41
+pub struct WindowInfo {
42
+    pub id: u32,
43
+    pub x: i16,
44
+    pub y: i16,
45
+    pub width: u16,
46
+    pub height: u16,
47
+    pub mapped: bool,
48
+    pub override_redirect: bool,
49
+}
50
+
51
+/// Events sent from gar WM to garchomp.
52
+#[derive(Debug, Clone, Serialize, Deserialize)]
53
+#[serde(tag = "type", rename_all = "snake_case")]
54
+pub enum GarEvent {
55
+    /// Workspace changed.
56
+    WorkspaceChanged {
57
+        from: usize,
58
+        to: usize,
59
+        direction: String,
60
+    },
61
+    /// Focus changed.
62
+    FocusChanged { old: Option<u32>, new: Option<u32> },
63
+    /// Window entered fullscreen.
64
+    WindowFullscreen { window: u32, fullscreen: bool },
65
+}
garchomp/Cargo.tomladded
@@ -0,0 +1,19 @@
1
+[package]
2
+name = "garchomp"
3
+description = "X11 compositor for gar desktop environment"
4
+version.workspace = true
5
+edition.workspace = true
6
+license.workspace = true
7
+
8
+[dependencies]
9
+garchomp-ipc = { workspace = true }
10
+x11rb = { workspace = true }
11
+tokio = { workspace = true }
12
+serde = { workspace = true }
13
+serde_json = { workspace = true }
14
+clap = { workspace = true, features = ["env"] }
15
+tracing = { workspace = true }
16
+tracing-subscriber = { workspace = true }
17
+thiserror = { workspace = true }
18
+anyhow = { workspace = true }
19
+ctrlc = "3"
garchomp/src/compositor/mod.rsadded
@@ -0,0 +1,344 @@
1
+//! Core compositor state and event handling.
2
+
3
+mod window;
4
+
5
+pub use window::{TrackedWindow, WindowType};
6
+
7
+use crate::x11::{CompositeExt, Connection};
8
+use std::collections::HashMap;
9
+use thiserror::Error;
10
+use x11rb::connection::Connection as _;
11
+use x11rb::protocol::damage::{ConnectionExt as DamageConnectionExt, ReportLevel};
12
+use x11rb::protocol::xproto::{
13
+    AtomEnum, ChangeWindowAttributesAux, ConfigureNotifyEvent, ConnectionExt, CreateNotifyEvent,
14
+    DestroyNotifyEvent, EventMask, MapNotifyEvent, PropertyNotifyEvent,
15
+    UnmapNotifyEvent, Window,
16
+};
17
+use x11rb::protocol::Event;
18
+
19
+#[derive(Error, Debug)]
20
+pub enum CompositorError {
21
+    #[error("X11 connection error: {0}")]
22
+    Connection(#[from] crate::x11::ConnectionError),
23
+
24
+    #[error("X11 error: {0}")]
25
+    X11(#[from] x11rb::errors::ConnectionError),
26
+
27
+    #[error("X11 reply error: {0}")]
28
+    Reply(#[from] x11rb::errors::ReplyError),
29
+}
30
+
31
+pub type Result<T> = std::result::Result<T, CompositorError>;
32
+
33
+/// The main compositor state.
34
+pub struct Compositor {
35
+    pub conn: Connection,
36
+    pub overlay: Window,
37
+    pub windows: HashMap<Window, TrackedWindow>,
38
+    pub running: bool,
39
+}
40
+
41
+impl Compositor {
42
+    /// Create a new compositor instance.
43
+    pub fn new() -> Result<Self> {
44
+        let conn = Connection::new()?;
45
+
46
+        // Redirect all windows for compositing
47
+        conn.redirect_subwindows()?;
48
+
49
+        // Get the overlay window
50
+        let overlay = conn.get_overlay_window()?;
51
+        conn.configure_overlay(overlay)?;
52
+
53
+        // Subscribe to events on root window
54
+        let event_mask = EventMask::SUBSTRUCTURE_NOTIFY
55
+            | EventMask::STRUCTURE_NOTIFY
56
+            | EventMask::PROPERTY_CHANGE;
57
+
58
+        conn.conn.change_window_attributes(
59
+            conn.root(),
60
+            &ChangeWindowAttributesAux::new().event_mask(event_mask),
61
+        )?;
62
+
63
+        conn.flush()?;
64
+
65
+        let mut compositor = Self {
66
+            conn,
67
+            overlay,
68
+            windows: HashMap::new(),
69
+            running: true,
70
+        };
71
+
72
+        // Scan existing windows
73
+        compositor.scan_windows()?;
74
+
75
+        Ok(compositor)
76
+    }
77
+
78
+    /// Scan for existing windows on startup.
79
+    fn scan_windows(&mut self) -> Result<()> {
80
+        let tree = self.conn.conn.query_tree(self.conn.root())?.reply()?;
81
+
82
+        // Collect windows to track (avoid borrow issues)
83
+        let mut to_track = Vec::new();
84
+        for window in tree.children {
85
+            if window == self.overlay {
86
+                continue;
87
+            }
88
+
89
+            if let Ok(attrs) = self.conn.conn.get_window_attributes(window) {
90
+                if let Ok(attrs) = attrs.reply() {
91
+                    if attrs.map_state != x11rb::protocol::xproto::MapState::UNMAPPED {
92
+                        to_track.push(window);
93
+                    }
94
+                }
95
+            }
96
+        }
97
+
98
+        for window in to_track {
99
+            self.track_window(window)?;
100
+        }
101
+
102
+        tracing::info!("Scanned {} existing windows", self.windows.len());
103
+        Ok(())
104
+    }
105
+
106
+    /// Start tracking a window.
107
+    fn track_window(&mut self, window: Window) -> Result<()> {
108
+        if self.windows.contains_key(&window) || window == self.overlay {
109
+            return Ok(());
110
+        }
111
+
112
+        // Get window geometry
113
+        let geom = self.conn.conn.get_geometry(window)?.reply()?;
114
+        let attrs = self.conn.conn.get_window_attributes(window)?.reply()?;
115
+
116
+        // Get window pixmap for compositing
117
+        let pixmap = self.conn.name_window_pixmap(window).ok();
118
+
119
+        // Create damage tracking for this window
120
+        let damage = self.conn.generate_id()?;
121
+        self.conn
122
+            .conn
123
+            .damage_create(damage, window, ReportLevel::NON_EMPTY)?;
124
+
125
+        // Subscribe to property changes on the window
126
+        self.conn.conn.change_window_attributes(
127
+            window,
128
+            &ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE),
129
+        )?;
130
+
131
+        let tracked = TrackedWindow {
132
+            id: window,
133
+            pixmap,
134
+            damage,
135
+            x: geom.x,
136
+            y: geom.y,
137
+            width: geom.width,
138
+            height: geom.height,
139
+            border_width: geom.border_width,
140
+            mapped: true,
141
+            override_redirect: attrs.override_redirect,
142
+            window_type: WindowType::Normal,
143
+            opacity: 1.0,
144
+            damaged: true,
145
+        };
146
+
147
+        tracing::debug!(
148
+            "Tracking window {:#x}: {}x{}+{}+{} override_redirect={}",
149
+            window,
150
+            geom.width,
151
+            geom.height,
152
+            geom.x,
153
+            geom.y,
154
+            attrs.override_redirect
155
+        );
156
+
157
+        self.windows.insert(window, tracked);
158
+        Ok(())
159
+    }
160
+
161
+    /// Stop tracking a window.
162
+    pub fn untrack_window(&mut self, window: Window) {
163
+        if let Some(tracked) = self.windows.remove(&window) {
164
+            // Destroy damage object
165
+            let _ = self.conn.conn.damage_destroy(tracked.damage);
166
+
167
+            // Free pixmap
168
+            if let Some(pixmap) = tracked.pixmap {
169
+                let _ = self.conn.conn.free_pixmap(pixmap);
170
+            }
171
+
172
+            tracing::debug!("Untracked window {:#x}", window);
173
+        }
174
+    }
175
+
176
+    /// Run the compositor event loop.
177
+    pub fn run(&mut self) -> Result<()> {
178
+        tracing::info!("Starting compositor event loop");
179
+
180
+        while self.running {
181
+            let event = self.conn.conn.wait_for_event()?;
182
+            self.handle_event(event)?;
183
+        }
184
+
185
+        Ok(())
186
+    }
187
+
188
+    /// Handle a single X11 event.
189
+    pub fn handle_event(&mut self, event: Event) -> Result<()> {
190
+        match event {
191
+            Event::CreateNotify(e) => self.handle_create(e)?,
192
+            Event::DestroyNotify(e) => self.handle_destroy(e),
193
+            Event::MapNotify(e) => self.handle_map(e)?,
194
+            Event::UnmapNotify(e) => self.handle_unmap(e),
195
+            Event::ConfigureNotify(e) => self.handle_configure(e)?,
196
+            Event::PropertyNotify(e) => self.handle_property(e)?,
197
+            Event::Error(e) => {
198
+                tracing::warn!("X11 error: {:?}", e);
199
+            }
200
+            Event::Unknown(raw) => {
201
+                // Check for damage events by response type (first byte)
202
+                if !raw.is_empty() {
203
+                    let response_type = raw[0] & 0x7f; // Mask out the "sent" flag
204
+                    if response_type == self.conn.damage_event_base {
205
+                        if raw.len() >= 8 {
206
+                            let drawable = u32::from_ne_bytes([raw[4], raw[5], raw[6], raw[7]]);
207
+                            if let Some(tracked) = self.windows.get_mut(&drawable) {
208
+                                tracked.damaged = true;
209
+                                let _ = self.conn.conn.damage_subtract(tracked.damage, 0u32, 0u32);
210
+                            }
211
+                        }
212
+                    }
213
+                }
214
+            }
215
+            _ => {}
216
+        }
217
+
218
+        Ok(())
219
+    }
220
+
221
+    fn handle_create(&mut self, event: CreateNotifyEvent) -> Result<()> {
222
+        tracing::trace!("CreateNotify: window {:#x}", event.window);
223
+        Ok(())
224
+    }
225
+
226
+    fn handle_destroy(&mut self, event: DestroyNotifyEvent) {
227
+        tracing::trace!("DestroyNotify: window {:#x}", event.window);
228
+        self.untrack_window(event.window);
229
+    }
230
+
231
+    fn handle_map(&mut self, event: MapNotifyEvent) -> Result<()> {
232
+        tracing::trace!("MapNotify: window {:#x}", event.window);
233
+
234
+        if let Some(tracked) = self.windows.get_mut(&event.window) {
235
+            tracked.mapped = true;
236
+            tracked.damaged = true;
237
+
238
+            // Get fresh pixmap
239
+            if let Some(old_pixmap) = tracked.pixmap.take() {
240
+                let _ = self.conn.conn.free_pixmap(old_pixmap);
241
+            }
242
+            tracked.pixmap = self.conn.name_window_pixmap(event.window).ok();
243
+        } else {
244
+            self.track_window(event.window)?;
245
+        }
246
+
247
+        Ok(())
248
+    }
249
+
250
+    fn handle_unmap(&mut self, event: UnmapNotifyEvent) {
251
+        tracing::trace!("UnmapNotify: window {:#x}", event.window);
252
+
253
+        if let Some(tracked) = self.windows.get_mut(&event.window) {
254
+            tracked.mapped = false;
255
+        }
256
+    }
257
+
258
+    fn handle_configure(&mut self, event: ConfigureNotifyEvent) -> Result<()> {
259
+        if let Some(tracked) = self.windows.get_mut(&event.window) {
260
+            let size_changed =
261
+                tracked.width != event.width || tracked.height != event.height;
262
+
263
+            tracked.x = event.x;
264
+            tracked.y = event.y;
265
+            tracked.width = event.width;
266
+            tracked.height = event.height;
267
+            tracked.border_width = event.border_width;
268
+            tracked.damaged = true;
269
+
270
+            if size_changed {
271
+                // Need new pixmap for resized window
272
+                if let Some(old_pixmap) = tracked.pixmap.take() {
273
+                    let _ = self.conn.conn.free_pixmap(old_pixmap);
274
+                }
275
+                if tracked.mapped {
276
+                    tracked.pixmap = self.conn.name_window_pixmap(event.window).ok();
277
+                }
278
+            }
279
+        }
280
+
281
+        Ok(())
282
+    }
283
+
284
+    fn handle_property(&mut self, event: PropertyNotifyEvent) -> Result<()> {
285
+        // Check for opacity changes
286
+        if event.atom == self.conn.atoms._NET_WM_WINDOW_OPACITY {
287
+            let opacity = self.get_window_opacity(event.window);
288
+            if let Some(tracked) = self.windows.get_mut(&event.window) {
289
+                tracked.opacity = opacity;
290
+                tracked.damaged = true;
291
+            }
292
+        }
293
+
294
+        Ok(())
295
+    }
296
+
297
+    fn get_window_opacity(&self, window: Window) -> f32 {
298
+        let cookie = match self.conn.conn.get_property(
299
+            false,
300
+            window,
301
+            self.conn.atoms._NET_WM_WINDOW_OPACITY,
302
+            AtomEnum::CARDINAL,
303
+            0,
304
+            1,
305
+        ) {
306
+            Ok(c) => c,
307
+            Err(_) => return 1.0,
308
+        };
309
+
310
+        match cookie.reply() {
311
+            Ok(reply) => {
312
+                if let Some(value) = reply.value32().and_then(|mut v| v.next()) {
313
+                    value as f32 / u32::MAX as f32
314
+                } else {
315
+                    1.0
316
+                }
317
+            }
318
+            Err(_) => 1.0,
319
+        }
320
+    }
321
+
322
+    /// Shutdown the compositor cleanly.
323
+    pub fn shutdown(&mut self) -> Result<()> {
324
+        tracing::info!("Shutting down compositor");
325
+
326
+        // Clean up tracked windows
327
+        let windows: Vec<Window> = self.windows.keys().copied().collect();
328
+        for window in windows {
329
+            self.untrack_window(window);
330
+        }
331
+
332
+        // Release overlay
333
+        self.conn.release_overlay_window()?;
334
+        self.conn.flush()?;
335
+
336
+        Ok(())
337
+    }
338
+}
339
+
340
+impl Drop for Compositor {
341
+    fn drop(&mut self) {
342
+        let _ = self.shutdown();
343
+    }
344
+}
garchomp/src/compositor/window.rsadded
@@ -0,0 +1,93 @@
1
+//! Window tracking types.
2
+
3
+use x11rb::protocol::xproto::{Pixmap, Window};
4
+
5
+/// Type of window for compositor effects.
6
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7
+pub enum WindowType {
8
+    Normal,
9
+    Desktop,
10
+    Dock,
11
+    Toolbar,
12
+    Menu,
13
+    Utility,
14
+    Splash,
15
+    Dialog,
16
+    DropdownMenu,
17
+    PopupMenu,
18
+    Tooltip,
19
+    Notification,
20
+    Combo,
21
+    Dnd,
22
+}
23
+
24
+impl Default for WindowType {
25
+    fn default() -> Self {
26
+        Self::Normal
27
+    }
28
+}
29
+
30
+/// A window being tracked by the compositor.
31
+#[derive(Debug)]
32
+pub struct TrackedWindow {
33
+    /// X11 window ID.
34
+    pub id: Window,
35
+    /// Pixmap containing window contents.
36
+    pub pixmap: Option<Pixmap>,
37
+    /// Damage object for tracking changes.
38
+    pub damage: u32,
39
+    /// X position.
40
+    pub x: i16,
41
+    /// Y position.
42
+    pub y: i16,
43
+    /// Width.
44
+    pub width: u16,
45
+    /// Height.
46
+    pub height: u16,
47
+    /// Border width.
48
+    pub border_width: u16,
49
+    /// Whether the window is mapped (visible).
50
+    pub mapped: bool,
51
+    /// Whether this is an override-redirect window (unmanaged).
52
+    pub override_redirect: bool,
53
+    /// Window type for effect decisions.
54
+    pub window_type: WindowType,
55
+    /// Window opacity (0.0 - 1.0).
56
+    pub opacity: f32,
57
+    /// Whether the window has been damaged since last render.
58
+    pub damaged: bool,
59
+}
60
+
61
+impl TrackedWindow {
62
+    /// Check if this window should have effects applied.
63
+    pub fn should_have_effects(&self) -> bool {
64
+        match self.window_type {
65
+            WindowType::Desktop | WindowType::Dock => false,
66
+            WindowType::Tooltip | WindowType::PopupMenu | WindowType::DropdownMenu => false,
67
+            _ => true,
68
+        }
69
+    }
70
+
71
+    /// Check if this window should have shadows.
72
+    pub fn should_have_shadow(&self) -> bool {
73
+        match self.window_type {
74
+            WindowType::Desktop | WindowType::Dock => false,
75
+            WindowType::Tooltip | WindowType::Menu => false,
76
+            _ => !self.override_redirect,
77
+        }
78
+    }
79
+
80
+    /// Check if this window should have blur behind.
81
+    pub fn should_have_blur(&self) -> bool {
82
+        self.should_have_effects() && self.opacity < 1.0
83
+    }
84
+
85
+    /// Check if this window should have rounded corners.
86
+    pub fn should_have_corners(&self) -> bool {
87
+        match self.window_type {
88
+            WindowType::Desktop | WindowType::Dock => false,
89
+            WindowType::Tooltip | WindowType::Menu => false,
90
+            _ => true,
91
+        }
92
+    }
93
+}
garchomp/src/ipc/mod.rsadded
@@ -0,0 +1,98 @@
1
+//! IPC server for garchomp control.
2
+
3
+use garchomp_ipc::{Request, Response};
4
+use std::io::{BufRead, BufReader, Write};
5
+use std::os::unix::net::{UnixListener, UnixStream};
6
+use std::path::PathBuf;
7
+use thiserror::Error;
8
+
9
+#[derive(Error, Debug)]
10
+pub enum IpcError {
11
+    #[error("IO error: {0}")]
12
+    Io(#[from] std::io::Error),
13
+
14
+    #[error("JSON error: {0}")]
15
+    Json(#[from] serde_json::Error),
16
+}
17
+
18
+pub type Result<T> = std::result::Result<T, IpcError>;
19
+
20
+/// IPC server for compositor control.
21
+pub struct IpcServer {
22
+    listener: UnixListener,
23
+    socket_path: PathBuf,
24
+}
25
+
26
+impl IpcServer {
27
+    /// Create a new IPC server.
28
+    pub fn new() -> Result<Self> {
29
+        let socket_path = Self::socket_path();
30
+
31
+        // Remove existing socket
32
+        let _ = std::fs::remove_file(&socket_path);
33
+
34
+        let listener = UnixListener::bind(&socket_path)?;
35
+        listener.set_nonblocking(true)?;
36
+
37
+        tracing::info!("IPC server listening at {:?}", socket_path);
38
+
39
+        Ok(Self {
40
+            listener,
41
+            socket_path,
42
+        })
43
+    }
44
+
45
+    /// Get the socket path.
46
+    fn socket_path() -> PathBuf {
47
+        let runtime_dir =
48
+            std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
49
+        PathBuf::from(runtime_dir).join("garchomp.sock")
50
+    }
51
+
52
+    /// Poll for incoming connections (non-blocking).
53
+    pub fn poll(&mut self) -> Option<ClientRequest> {
54
+        match self.listener.accept() {
55
+            Ok((stream, _)) => {
56
+                if let Ok(request) = Self::read_request(&stream) {
57
+                    Some(ClientRequest { stream, request })
58
+                } else {
59
+                    None
60
+                }
61
+            }
62
+            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => None,
63
+            Err(e) => {
64
+                tracing::warn!("IPC accept error: {}", e);
65
+                None
66
+            }
67
+        }
68
+    }
69
+
70
+    fn read_request(stream: &UnixStream) -> Result<Request> {
71
+        let mut reader = BufReader::new(stream);
72
+        let mut line = String::new();
73
+        reader.read_line(&mut line)?;
74
+        Ok(serde_json::from_str(&line)?)
75
+    }
76
+}
77
+
78
+impl Drop for IpcServer {
79
+    fn drop(&mut self) {
80
+        let _ = std::fs::remove_file(&self.socket_path);
81
+    }
82
+}
83
+
84
+/// A client request with the stream for responding.
85
+pub struct ClientRequest {
86
+    stream: UnixStream,
87
+    pub request: Request,
88
+}
89
+
90
+impl ClientRequest {
91
+    /// Send a response to the client.
92
+    pub fn respond(mut self, response: Response) -> Result<()> {
93
+        let mut msg = serde_json::to_string(&response)?;
94
+        msg.push('\n');
95
+        self.stream.write_all(msg.as_bytes())?;
96
+        Ok(())
97
+    }
98
+}
garchomp/src/main.rsadded
@@ -0,0 +1,165 @@
1
+//! garchomp - X11 compositor for gar desktop environment.
2
+
3
+mod compositor;
4
+mod ipc;
5
+mod x11;
6
+
7
+use anyhow::{Context, Result};
8
+use clap::Parser;
9
+use garchomp_ipc::{Request, Response, WindowInfo};
10
+use std::sync::atomic::{AtomicBool, Ordering};
11
+use std::sync::Arc;
12
+use tracing_subscriber::EnvFilter;
13
+use x11rb::connection::Connection as _;
14
+
15
+#[derive(Parser)]
16
+#[command(name = "garchomp", about = "X11 compositor for gar desktop environment")]
17
+struct Cli {
18
+    /// X11 display to connect to.
19
+    #[arg(short, long, env = "DISPLAY")]
20
+    display: Option<String>,
21
+
22
+    /// Path to configuration file.
23
+    #[arg(short, long, default_value = "~/.config/gar/init.lua")]
24
+    config: String,
25
+
26
+    /// Increase logging verbosity.
27
+    #[arg(short, long, action = clap::ArgAction::Count)]
28
+    verbose: u8,
29
+
30
+    /// Disable HDR support.
31
+    #[arg(long)]
32
+    no_hdr: bool,
33
+}
34
+
35
+fn main() -> Result<()> {
36
+    let cli = Cli::parse();
37
+
38
+    // Initialize logging
39
+    init_logging(cli.verbose);
40
+
41
+    tracing::info!("garchomp compositor starting");
42
+
43
+    // Set up signal handling
44
+    let running = Arc::new(AtomicBool::new(true));
45
+    setup_signal_handlers(running.clone())?;
46
+
47
+    // Create compositor
48
+    let mut compositor =
49
+        compositor::Compositor::new().context("Failed to initialize compositor")?;
50
+
51
+    // Create IPC server
52
+    let mut ipc_server = ipc::IpcServer::new().context("Failed to start IPC server")?;
53
+
54
+    tracing::info!(
55
+        "Compositor initialized, tracking {} windows",
56
+        compositor.windows.len()
57
+    );
58
+
59
+    // Main event loop
60
+    while running.load(Ordering::Relaxed) && compositor.running {
61
+        // Handle IPC requests (non-blocking)
62
+        while let Some(request) = ipc_server.poll() {
63
+            handle_ipc_request(&mut compositor, request);
64
+        }
65
+
66
+        // Handle X11 events (non-blocking poll)
67
+        if let Ok(Some(event)) = compositor.conn.conn.poll_for_event() {
68
+            if let Err(e) = compositor.handle_event(event) {
69
+                tracing::error!("Error handling event: {}", e);
70
+            }
71
+        }
72
+
73
+        // Small sleep to prevent busy-waiting
74
+        // TODO: Use proper event-driven approach with poll/select
75
+        std::thread::sleep(std::time::Duration::from_millis(1));
76
+    }
77
+
78
+    tracing::info!("Shutting down");
79
+    compositor.shutdown()?;
80
+
81
+    Ok(())
82
+}
83
+
84
+fn init_logging(verbosity: u8) {
85
+    let filter = match verbosity {
86
+        0 => "garchomp=info",
87
+        1 => "garchomp=debug",
88
+        _ => "garchomp=trace",
89
+    };
90
+
91
+    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter));
92
+
93
+    tracing_subscriber::fmt()
94
+        .with_env_filter(filter)
95
+        .with_target(false)
96
+        .init();
97
+}
98
+
99
+fn setup_signal_handlers(running: Arc<AtomicBool>) -> Result<()> {
100
+    ctrlc::set_handler(move || {
101
+        tracing::info!("Received shutdown signal");
102
+        running.store(false, Ordering::Relaxed);
103
+    })
104
+    .context("Failed to set signal handler")?;
105
+
106
+    Ok(())
107
+}
108
+
109
+fn handle_ipc_request(compositor: &mut compositor::Compositor, request: ipc::ClientRequest) {
110
+    let response = match &request.request {
111
+        Request::Ping => Response::Pong,
112
+        Request::Reload => {
113
+            tracing::info!("Config reload requested");
114
+            // TODO: Implement config reload
115
+            Response::Ok
116
+        }
117
+        Request::SetEffect { effect, enabled } => {
118
+            tracing::info!("Set effect {} = {}", effect, enabled);
119
+            // TODO: Implement effect toggle
120
+            Response::Ok
121
+        }
122
+        Request::SetBlurStrength { strength } => {
123
+            tracing::info!("Set blur strength = {}", strength);
124
+            // TODO: Implement blur strength
125
+            Response::Ok
126
+        }
127
+        Request::GetWindowInfo { window } => {
128
+            if let Some(tracked) = compositor.windows.get(&(*window as u32)) {
129
+                Response::WindowInfo(WindowInfo {
130
+                    id: tracked.id,
131
+                    x: tracked.x,
132
+                    y: tracked.y,
133
+                    width: tracked.width,
134
+                    height: tracked.height,
135
+                    mapped: tracked.mapped,
136
+                    override_redirect: tracked.override_redirect,
137
+                })
138
+            } else {
139
+                Response::Error {
140
+                    message: "Window not found".into(),
141
+                }
142
+            }
143
+        }
144
+        Request::ListWindows => {
145
+            let windows: Vec<WindowInfo> = compositor
146
+                .windows
147
+                .values()
148
+                .map(|w| WindowInfo {
149
+                    id: w.id,
150
+                    x: w.x,
151
+                    y: w.y,
152
+                    width: w.width,
153
+                    height: w.height,
154
+                    mapped: w.mapped,
155
+                    override_redirect: w.override_redirect,
156
+                })
157
+                .collect();
158
+            Response::WindowList { windows }
159
+        }
160
+    };
161
+
162
+    if let Err(e) = request.respond(response) {
163
+        tracing::warn!("Failed to send IPC response: {}", e);
164
+    }
165
+}
garchomp/src/x11/atoms.rsadded
@@ -0,0 +1,63 @@
1
+//! X11 atom definitions for EWMH and compositor protocols.
2
+
3
+use x11rb::atom_manager;
4
+
5
+atom_manager! {
6
+    /// Atoms used by garchomp.
7
+    pub Atoms: AtomsCookie {
8
+        // ICCCM
9
+        WM_PROTOCOLS,
10
+        WM_DELETE_WINDOW,
11
+        WM_STATE,
12
+        WM_TRANSIENT_FOR,
13
+
14
+        // EWMH - Window types
15
+        _NET_WM_WINDOW_TYPE,
16
+        _NET_WM_WINDOW_TYPE_DESKTOP,
17
+        _NET_WM_WINDOW_TYPE_DOCK,
18
+        _NET_WM_WINDOW_TYPE_TOOLBAR,
19
+        _NET_WM_WINDOW_TYPE_MENU,
20
+        _NET_WM_WINDOW_TYPE_UTILITY,
21
+        _NET_WM_WINDOW_TYPE_SPLASH,
22
+        _NET_WM_WINDOW_TYPE_DIALOG,
23
+        _NET_WM_WINDOW_TYPE_DROPDOWN_MENU,
24
+        _NET_WM_WINDOW_TYPE_POPUP_MENU,
25
+        _NET_WM_WINDOW_TYPE_TOOLTIP,
26
+        _NET_WM_WINDOW_TYPE_NOTIFICATION,
27
+        _NET_WM_WINDOW_TYPE_COMBO,
28
+        _NET_WM_WINDOW_TYPE_DND,
29
+        _NET_WM_WINDOW_TYPE_NORMAL,
30
+
31
+        // EWMH - Window state
32
+        _NET_WM_STATE,
33
+        _NET_WM_STATE_MODAL,
34
+        _NET_WM_STATE_STICKY,
35
+        _NET_WM_STATE_MAXIMIZED_VERT,
36
+        _NET_WM_STATE_MAXIMIZED_HORZ,
37
+        _NET_WM_STATE_SHADED,
38
+        _NET_WM_STATE_SKIP_TASKBAR,
39
+        _NET_WM_STATE_SKIP_PAGER,
40
+        _NET_WM_STATE_HIDDEN,
41
+        _NET_WM_STATE_FULLSCREEN,
42
+        _NET_WM_STATE_ABOVE,
43
+        _NET_WM_STATE_BELOW,
44
+        _NET_WM_STATE_DEMANDS_ATTENTION,
45
+        _NET_WM_STATE_FOCUSED,
46
+
47
+        // EWMH - Active window
48
+        _NET_ACTIVE_WINDOW,
49
+        _NET_CLIENT_LIST,
50
+        _NET_CLIENT_LIST_STACKING,
51
+
52
+        // Compositor bypass
53
+        _NET_WM_BYPASS_COMPOSITOR,
54
+
55
+        // Opacity
56
+        _NET_WM_WINDOW_OPACITY,
57
+
58
+        // Compositor-specific
59
+        _GARCHOMP_COLORSPACE,
60
+        _GARCHOMP_MAX_LUMINANCE,
61
+        _GARCHOMP_MIN_LUMINANCE,
62
+    }
63
+}
garchomp/src/x11/composite.rsadded
@@ -0,0 +1,121 @@
1
+//! X11 Composite extension operations.
2
+
3
+use super::connection::{Connection, Result};
4
+use x11rb::protocol::composite::{ConnectionExt as _, Redirect};
5
+use x11rb::protocol::xfixes::ConnectionExt as XfixesConnectionExt;
6
+use x11rb::protocol::xproto::{
7
+    AtomEnum, ConfigureWindowAux, ConnectionExt as _, Pixmap, StackMode, Window,
8
+};
9
+
10
+/// Extension trait for Composite operations on Connection.
11
+pub trait CompositeExt {
12
+    /// Redirect all subwindows of root for compositing.
13
+    fn redirect_subwindows(&self) -> Result<()>;
14
+
15
+    /// Get the composite overlay window.
16
+    fn get_overlay_window(&self) -> Result<Window>;
17
+
18
+    /// Release the overlay window.
19
+    fn release_overlay_window(&self) -> Result<()>;
20
+
21
+    /// Get a pixmap for a window's contents.
22
+    fn name_window_pixmap(&self, window: Window) -> Result<Pixmap>;
23
+
24
+    /// Unredirect a window (for fullscreen bypass).
25
+    fn unredirect_window(&self, window: Window) -> Result<()>;
26
+
27
+    /// Re-redirect a window.
28
+    fn redirect_window(&self, window: Window) -> Result<()>;
29
+
30
+    /// Configure the overlay window to cover the screen.
31
+    fn configure_overlay(&self, overlay: Window) -> Result<()>;
32
+
33
+    /// Check if a window wants to bypass the compositor.
34
+    fn wants_bypass(&self, window: Window) -> Result<bool>;
35
+}
36
+
37
+impl CompositeExt for Connection {
38
+    fn redirect_subwindows(&self) -> Result<()> {
39
+        self.conn
40
+            .composite_redirect_subwindows(self.root(), Redirect::AUTOMATIC)?;
41
+        tracing::debug!("Redirected subwindows for compositing");
42
+        Ok(())
43
+    }
44
+
45
+    fn get_overlay_window(&self) -> Result<Window> {
46
+        let reply = self.conn.composite_get_overlay_window(self.root())?.reply()?;
47
+        tracing::debug!("Got overlay window: {:#x}", reply.overlay_win);
48
+        Ok(reply.overlay_win)
49
+    }
50
+
51
+    fn release_overlay_window(&self) -> Result<()> {
52
+        self.conn.composite_release_overlay_window(self.root())?;
53
+        tracing::debug!("Released overlay window");
54
+        Ok(())
55
+    }
56
+
57
+    fn name_window_pixmap(&self, window: Window) -> Result<Pixmap> {
58
+        let pixmap = self.generate_id()?;
59
+        self.conn.composite_name_window_pixmap(window, pixmap)?;
60
+        Ok(pixmap)
61
+    }
62
+
63
+    fn unredirect_window(&self, window: Window) -> Result<()> {
64
+        self.conn
65
+            .composite_unredirect_window(window, Redirect::AUTOMATIC)?;
66
+        tracing::debug!("Unredirected window {:#x}", window);
67
+        Ok(())
68
+    }
69
+
70
+    fn redirect_window(&self, window: Window) -> Result<()> {
71
+        self.conn
72
+            .composite_redirect_window(window, Redirect::AUTOMATIC)?;
73
+        tracing::debug!("Redirected window {:#x}", window);
74
+        Ok(())
75
+    }
76
+
77
+    fn configure_overlay(&self, overlay: Window) -> Result<()> {
78
+        let screen = self.screen();
79
+
80
+        // Make overlay cover the entire screen
81
+        let aux = ConfigureWindowAux::new()
82
+            .x(0)
83
+            .y(0)
84
+            .width(screen.width_in_pixels as u32)
85
+            .height(screen.height_in_pixels as u32)
86
+            .stack_mode(StackMode::ABOVE);
87
+
88
+        self.conn.configure_window(overlay, &aux)?;
89
+
90
+        // Allow input to pass through to windows below
91
+        let region = self.generate_id()?;
92
+        self.conn.xfixes_create_region(region, &[])?;
93
+        self.conn.xfixes_set_window_shape_region(overlay, x11rb::protocol::shape::SK::INPUT, 0, 0, region)?;
94
+        self.conn.xfixes_destroy_region(region)?;
95
+
96
+        tracing::debug!(
97
+            "Configured overlay: {}x{}",
98
+            screen.width_in_pixels,
99
+            screen.height_in_pixels
100
+        );
101
+        Ok(())
102
+    }
103
+
104
+    fn wants_bypass(&self, window: Window) -> Result<bool> {
105
+        let reply = self.conn.get_property(
106
+            false,
107
+            window,
108
+            self.atoms._NET_WM_BYPASS_COMPOSITOR,
109
+            AtomEnum::CARDINAL,
110
+            0,
111
+            1,
112
+        )?.reply()?;
113
+
114
+        if let Some(value) = reply.value32().and_then(|mut v| v.next()) {
115
+            // 1 = bypass requested, 2 = bypass when fullscreen
116
+            Ok(value >= 1)
117
+        } else {
118
+            Ok(false)
119
+        }
120
+    }
121
+}
garchomp/src/x11/connection.rsadded
@@ -0,0 +1,131 @@
1
+//! X11 connection wrapper with extension support.
2
+
3
+use super::Atoms;
4
+use thiserror::Error;
5
+use x11rb::connection::{Connection as X11Connection, RequestConnection};
6
+use x11rb::protocol::composite::ConnectionExt as CompositeConnectionExt;
7
+use x11rb::protocol::damage::ConnectionExt as DamageConnectionExt;
8
+use x11rb::protocol::xfixes::ConnectionExt as XfixesConnectionExt;
9
+use x11rb::protocol::xproto::{Screen, Window};
10
+use x11rb::rust_connection::RustConnection;
11
+
12
+#[derive(Error, Debug)]
13
+pub enum ConnectionError {
14
+    #[error("failed to connect to X11 display")]
15
+    Connect(#[from] x11rb::errors::ConnectError),
16
+
17
+    #[error("X11 connection error: {0}")]
18
+    Connection(#[from] x11rb::errors::ConnectionError),
19
+
20
+    #[error("X11 reply error: {0}")]
21
+    Reply(#[from] x11rb::errors::ReplyError),
22
+
23
+    #[error("extension '{name}' not available (required version: {required})")]
24
+    ExtensionMissing { name: &'static str, required: &'static str },
25
+
26
+    #[error("failed to intern atoms")]
27
+    Atoms(#[from] x11rb::errors::ReplyOrIdError),
28
+}
29
+
30
+pub type Result<T> = std::result::Result<T, ConnectionError>;
31
+
32
+/// X11 connection with compositor extensions.
33
+pub struct Connection {
34
+    pub conn: RustConnection,
35
+    pub screen_num: usize,
36
+    pub atoms: Atoms,
37
+    pub composite_opcode: u8,
38
+    pub damage_opcode: u8,
39
+    pub damage_event_base: u8,
40
+}
41
+
42
+impl Connection {
43
+    /// Connect to the X11 display and initialize extensions.
44
+    pub fn new() -> Result<Self> {
45
+        let (conn, screen_num) = x11rb::connect(None)?;
46
+
47
+        // Query Composite extension
48
+        let composite = conn
49
+            .composite_query_version(0, 4)?
50
+            .reply()?;
51
+        tracing::info!(
52
+            "Composite extension: {}.{}",
53
+            composite.major_version,
54
+            composite.minor_version
55
+        );
56
+        if composite.major_version == 0 && composite.minor_version < 4 {
57
+            return Err(ConnectionError::ExtensionMissing {
58
+                name: "Composite",
59
+                required: "0.4",
60
+            });
61
+        }
62
+
63
+        // Query Damage extension
64
+        let damage = conn
65
+            .damage_query_version(1, 1)?
66
+            .reply()?;
67
+        tracing::info!(
68
+            "Damage extension: {}.{}",
69
+            damage.major_version,
70
+            damage.minor_version
71
+        );
72
+
73
+        // Query XFixes extension
74
+        let xfixes = conn
75
+            .xfixes_query_version(5, 0)?
76
+            .reply()?;
77
+        tracing::info!(
78
+            "XFixes extension: {}.{}",
79
+            xfixes.major_version,
80
+            xfixes.minor_version
81
+        );
82
+
83
+        // Get extension opcodes for event handling
84
+        let composite_ext = conn
85
+            .extension_information(x11rb::protocol::composite::X11_EXTENSION_NAME)?
86
+            .ok_or(ConnectionError::ExtensionMissing {
87
+                name: "Composite",
88
+                required: "0.4",
89
+            })?;
90
+
91
+        let damage_ext = conn
92
+            .extension_information(x11rb::protocol::damage::X11_EXTENSION_NAME)?
93
+            .ok_or(ConnectionError::ExtensionMissing {
94
+                name: "Damage",
95
+                required: "1.1",
96
+            })?;
97
+
98
+        // Intern atoms
99
+        let atoms = Atoms::new(&conn)?.reply()?;
100
+
101
+        Ok(Self {
102
+            conn,
103
+            screen_num,
104
+            atoms,
105
+            composite_opcode: composite_ext.major_opcode,
106
+            damage_opcode: damage_ext.major_opcode,
107
+            damage_event_base: damage_ext.first_event,
108
+        })
109
+    }
110
+
111
+    /// Get the default screen.
112
+    pub fn screen(&self) -> &Screen {
113
+        &self.conn.setup().roots[self.screen_num]
114
+    }
115
+
116
+    /// Get the root window.
117
+    pub fn root(&self) -> Window {
118
+        self.screen().root
119
+    }
120
+
121
+    /// Flush pending requests to the server.
122
+    pub fn flush(&self) -> Result<()> {
123
+        self.conn.flush()?;
124
+        Ok(())
125
+    }
126
+
127
+    /// Generate a new X11 ID.
128
+    pub fn generate_id(&self) -> Result<u32> {
129
+        Ok(self.conn.generate_id()?)
130
+    }
131
+}
garchomp/src/x11/mod.rsadded
@@ -0,0 +1,9 @@
1
+//! X11 protocol layer for garchomp compositor.
2
+
3
+mod atoms;
4
+mod composite;
5
+mod connection;
6
+
7
+pub use atoms::Atoms;
8
+pub use composite::CompositeExt;
9
+pub use connection::{Connection, ConnectionError};
garchompctl/Cargo.tomladded
@@ -0,0 +1,12 @@
1
+[package]
2
+name = "garchompctl"
3
+description = "Control utility for garchomp compositor"
4
+version.workspace = true
5
+edition.workspace = true
6
+license.workspace = true
7
+
8
+[dependencies]
9
+garchomp-ipc = { workspace = true }
10
+clap = { workspace = true }
11
+serde_json = { workspace = true }
12
+anyhow = { workspace = true }
garchompctl/src/main.rsadded
@@ -0,0 +1,98 @@
1
+//! garchompctl - Control utility for garchomp compositor.
2
+
3
+use anyhow::{Context, Result};
4
+use clap::{Parser, Subcommand};
5
+use garchomp_ipc::{Request, Response};
6
+use std::io::{BufRead, BufReader, Write};
7
+use std::os::unix::net::UnixStream;
8
+
9
+#[derive(Parser)]
10
+#[command(name = "garchompctl", about = "Control garchomp compositor")]
11
+struct Cli {
12
+    #[command(subcommand)]
13
+    command: Commands,
14
+}
15
+
16
+#[derive(Subcommand)]
17
+enum Commands {
18
+    /// Reload configuration.
19
+    Reload,
20
+    /// Enable an effect.
21
+    Enable { effect: String },
22
+    /// Disable an effect.
23
+    Disable { effect: String },
24
+    /// Set blur strength (0-20).
25
+    Blur { strength: u32 },
26
+    /// List managed windows.
27
+    Windows,
28
+    /// Ping the compositor.
29
+    Ping,
30
+}
31
+
32
+fn main() -> Result<()> {
33
+    let cli = Cli::parse();
34
+
35
+    let request = match cli.command {
36
+        Commands::Reload => Request::Reload,
37
+        Commands::Enable { effect } => Request::SetEffect {
38
+            effect,
39
+            enabled: true,
40
+        },
41
+        Commands::Disable { effect } => Request::SetEffect {
42
+            effect,
43
+            enabled: false,
44
+        },
45
+        Commands::Blur { strength } => Request::SetBlurStrength { strength },
46
+        Commands::Windows => Request::ListWindows,
47
+        Commands::Ping => Request::Ping,
48
+    };
49
+
50
+    let response = send_request(&request)?;
51
+
52
+    match response {
53
+        Response::Ok => println!("OK"),
54
+        Response::Pong => println!("pong"),
55
+        Response::Error { message } => {
56
+            eprintln!("Error: {}", message);
57
+            std::process::exit(1);
58
+        }
59
+        Response::WindowInfo(info) => {
60
+            println!("{}", serde_json::to_string_pretty(&info)?);
61
+        }
62
+        Response::WindowList { windows } => {
63
+            for win in windows {
64
+                println!(
65
+                    "{:#010x}  {}x{}+{}+{}  {}",
66
+                    win.id,
67
+                    win.width,
68
+                    win.height,
69
+                    win.x,
70
+                    win.y,
71
+                    if win.mapped { "mapped" } else { "unmapped" }
72
+                );
73
+            }
74
+        }
75
+    }
76
+
77
+    Ok(())
78
+}
79
+
80
+fn send_request(request: &Request) -> Result<Response> {
81
+    let socket_path = format!(
82
+        "{}/garchomp.sock",
83
+        std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into())
84
+    );
85
+
86
+    let mut stream =
87
+        UnixStream::connect(&socket_path).context("Failed to connect to garchomp socket")?;
88
+
89
+    let mut msg = serde_json::to_string(request)?;
90
+    msg.push('\n');
91
+    stream.write_all(msg.as_bytes())?;
92
+
93
+    let mut reader = BufReader::new(stream);
94
+    let mut line = String::new();
95
+    reader.read_line(&mut line)?;
96
+
97
+    serde_json::from_str(&line).context("Failed to parse response")
98
+}