markdown · 12636 bytes Raw Blame History

Sprint 8: EWMH Compliance + Polish

Goal: Full EWMH compliance for ecosystem integration, plus visual polish.

Objectives

  • Complete EWMH atom support for tools like polybar, rofi, dunst
  • Proper ICCCM compliance
  • Visual features: borders, gaps, optional title bars
  • Urgency hints

Prerequisites

  • Sprint 7 complete (multi-monitor)

EWMH Compliance

Required Atoms

Atom Purpose Implementation
_NET_SUPPORTED List of supported atoms Set on root
_NET_SUPPORTING_WM_CHECK WM identification Child window
_NET_CLIENT_LIST All managed windows Update on changes
_NET_CLIENT_LIST_STACKING Windows in stacking order Update on changes
_NET_NUMBER_OF_DESKTOPS Workspace count Set on init
_NET_DESKTOP_GEOMETRY Virtual desktop size Set on init
_NET_DESKTOP_VIEWPORT Viewport position Per workspace
_NET_CURRENT_DESKTOP Active workspace Update on switch
_NET_DESKTOP_NAMES Workspace names Set on init
_NET_ACTIVE_WINDOW Focused window Update on focus
_NET_WORKAREA Usable screen area Per monitor
_NET_WM_NAME WM name "gar"
_NET_WM_DESKTOP Window's workspace Per window
_NET_WM_STATE Window states Per window
_NET_WM_WINDOW_TYPE Window type Read, respect

Tasks

8.1 EWMH Setup

  • Create src/x11/ewmh.rs
  • Intern all EWMH atoms on startup
  • Create supporting WM check window
  • Set _NET_SUPPORTED with all supported atoms
  • Set WM name
pub struct EwmhAtoms {
    pub _NET_SUPPORTED: Atom,
    pub _NET_SUPPORTING_WM_CHECK: Atom,
    pub _NET_CLIENT_LIST: Atom,
    pub _NET_CLIENT_LIST_STACKING: Atom,
    pub _NET_NUMBER_OF_DESKTOPS: Atom,
    pub _NET_CURRENT_DESKTOP: Atom,
    pub _NET_DESKTOP_NAMES: Atom,
    pub _NET_ACTIVE_WINDOW: Atom,
    pub _NET_WM_NAME: Atom,
    pub _NET_WM_DESKTOP: Atom,
    pub _NET_WM_STATE: Atom,
    pub _NET_WM_STATE_FULLSCREEN: Atom,
    pub _NET_WM_STATE_HIDDEN: Atom,
    pub _NET_WM_STATE_DEMANDS_ATTENTION: Atom,
    pub _NET_WM_WINDOW_TYPE: Atom,
    pub _NET_WM_WINDOW_TYPE_DIALOG: Atom,
    // ... more
}

fn setup_ewmh(conn: &impl Connection, root: Window, atoms: &EwmhAtoms) -> Result<()> {
    // Create check window
    let check_window = conn.generate_id()?;
    conn.create_window(
        0, check_window, root,
        -1, -1, 1, 1, 0,
        WindowClass::INPUT_OUTPUT,
        0,
        &CreateWindowAux::new(),
    )?;

    // Set supporting WM check
    conn.change_property32(
        PropMode::REPLACE, root,
        atoms._NET_SUPPORTING_WM_CHECK,
        AtomEnum::WINDOW,
        &[check_window],
    )?;
    conn.change_property32(
        PropMode::REPLACE, check_window,
        atoms._NET_SUPPORTING_WM_CHECK,
        AtomEnum::WINDOW,
        &[check_window],
    )?;

    // Set WM name
    conn.change_property8(
        PropMode::REPLACE, check_window,
        atoms._NET_WM_NAME,
        atoms.UTF8_STRING,
        b"gar",
    )?;

    // Set supported atoms
    let supported = vec![
        atoms._NET_SUPPORTED,
        atoms._NET_SUPPORTING_WM_CHECK,
        // ... all supported atoms
    ];
    conn.change_property32(
        PropMode::REPLACE, root,
        atoms._NET_SUPPORTED,
        AtomEnum::ATOM,
        &supported,
    )?;

    Ok(())
}

8.2 Client List Maintenance

  • Update _NET_CLIENT_LIST on window add/remove
  • Update _NET_CLIENT_LIST_STACKING on stacking changes
  • Update _NET_ACTIVE_WINDOW on focus change
fn update_client_list(&self, conn: &impl Connection) -> Result<()> {
    let windows: Vec<u32> = self.all_windows()
        .iter()
        .map(|w| w.id)
        .collect();

    conn.change_property32(
        PropMode::REPLACE,
        self.root,
        self.atoms._NET_CLIENT_LIST,
        AtomEnum::WINDOW,
        &windows,
    )?;

    Ok(())
}

8.3 Handle Client Messages

  • Handle _NET_ACTIVE_WINDOW (focus requests from apps)
  • Handle _NET_WM_STATE changes (fullscreen, etc.)
  • Handle _NET_CURRENT_DESKTOP (pager workspace switch)
  • Handle _NET_CLOSE_WINDOW (close requests)
fn handle_client_message(&mut self, event: ClientMessageEvent) -> Result<()> {
    let atom = event.type_;

    if atom == self.atoms._NET_ACTIVE_WINDOW {
        // Application requesting focus
        let window = event.window;
        self.focus_window(window)?;
    } else if atom == self.atoms._NET_WM_STATE {
        // State change request (e.g., fullscreen)
        let action = event.data.as_data32()[0];
        let prop = event.data.as_data32()[1];
        self.handle_state_change(event.window, action, prop)?;
    } else if atom == self.atoms._NET_CURRENT_DESKTOP {
        // Workspace switch request
        let desktop = event.data.as_data32()[0] as usize;
        self.switch_workspace(WorkspaceId(desktop + 1))?;
    }

    Ok(())
}

8.4 Window State Management

  • Track window states (fullscreen, hidden, urgent)
  • Update _NET_WM_STATE property
  • Implement fullscreen toggle (Mod+F)
  • Handle fullscreen requests from applications
fn set_fullscreen(&mut self, window: WindowId, fullscreen: bool) -> Result<()> {
    let win = self.get_window_mut(window)?;

    if fullscreen {
        // Save current geometry
        win.saved_geometry = Some(win.geometry);
        // Set to monitor size
        let monitor = self.get_monitor_for_window(window);
        win.geometry = monitor.geometry;
        win.states.insert(WindowState::Fullscreen);
    } else {
        // Restore geometry
        if let Some(saved) = win.saved_geometry.take() {
            win.geometry = saved;
        }
        win.states.remove(&WindowState::Fullscreen);
    }

    // Update X property
    self.update_wm_state(window)?;
    self.apply_geometry(window)?;

    Ok(())
}

8.5 ICCCM Compliance

  • Create src/x11/icccm.rs
  • Handle WM_HINTS (urgency, input, etc.)
  • Handle WM_NORMAL_HINTS (size hints)
  • Handle WM_PROTOCOLS (delete window, take focus)
  • Handle WM_TRANSIENT_FOR
fn get_wm_hints(conn: &impl Connection, window: Window) -> Result<Option<WmHints>> {
    // Read WM_HINTS property
    // Parse into struct
}

fn get_size_hints(conn: &impl Connection, window: Window) -> Result<Option<SizeHints>> {
    // Read WM_NORMAL_HINTS
    // Extract min/max size, aspect ratio, etc.
}

8.6 Border Colors

  • Configurable border colors via Lua
  • Different colors: focused, unfocused, urgent
  • Apply colors on focus change
  • Flash urgent windows
fn update_border_color(&self, conn: &impl Connection, window: WindowId) -> Result<()> {
    let win = self.get_window(window)?;
    let color = if win.urgent {
        self.config.border_color_urgent
    } else if Some(window) == self.focused {
        self.config.border_color_focused
    } else {
        self.config.border_color_unfocused
    };

    conn.change_window_attributes(
        window,
        &ChangeWindowAttributesAux::new().border_pixel(color),
    )?;

    Ok(())
}

8.7 Gaps

  • Inner gaps (between windows)
  • Outer gaps (screen edge)
  • Configurable via Lua
  • Apply in geometry calculation
fn calculate_geometries_with_gaps(&self, rect: Rect, gaps: &GapConfig) -> Vec<(WindowId, Rect)> {
    // Shrink rect by outer gaps
    let work_area = Rect {
        x: rect.x + gaps.outer as i16,
        y: rect.y + gaps.outer as i16,
        width: rect.width - 2 * gaps.outer,
        height: rect.height - 2 * gaps.outer,
    };

    // Calculate from tree
    let mut geometries = self.tree.calculate_geometries(work_area);

    // Apply inner gaps
    for (_, geom) in &mut geometries {
        geom.x += gaps.inner as i16 / 2;
        geom.y += gaps.inner as i16 / 2;
        geom.width -= gaps.inner;
        geom.height -= gaps.inner;
    }

    geometries
}

8.8 Optional Title Bars

  • Create title bar windows (reparenting)
  • Draw window title
  • Handle title bar clicks (focus, move)
  • Configurable per window class
  • Double-click to toggle maximize
fn create_frame(&mut self, conn: &impl Connection, window: WindowId) -> Result<Frame> {
    let title_height = if self.should_have_titlebar(window) {
        self.config.titlebar_height
    } else {
        0
    };

    // Create frame window
    let frame = conn.generate_id()?;
    conn.create_window(/* ... */)?;

    // Reparent client into frame
    conn.reparent_window(window, frame, 0, title_height)?;

    Ok(Frame { id: frame, client: window, title_height })
}

8.9 Urgency Hints

  • Read WM_HINTS urgency flag
  • Read _NET_WM_STATE_DEMANDS_ATTENTION
  • Set urgent border color
  • Indicate urgent workspace (for status bars)
  • Clear urgency on focus
fn handle_urgency(&mut self, window: WindowId) -> Result<()> {
    let hints = get_wm_hints(&self.conn, window)?;

    if hints.map(|h| h.urgent).unwrap_or(false) {
        let win = self.get_window_mut(window)?;
        win.urgent = true;

        // Update workspace urgency
        let ws = self.get_workspace_for_window(window)?;
        self.workspaces[ws].urgent = true;

        // Update EWMH
        self.set_wm_state(window, StateAction::Add, self.atoms._NET_WM_STATE_DEMANDS_ATTENTION)?;

        // Update border
        self.update_border_color(window)?;
    }

    Ok(())
}

8.10 External Compositor Integration

Goal: Support external compositors (picom) for proper screen repainting and visual effects.

Background

Like i3, gar is NOT a compositing window manager. We rely on external compositors (picom, compton, xcompmgr) for:

  • Proper screen repainting when windows close
  • Transparency and visual effects
  • Vsync and tear-free rendering

Without a compositor, X11 does not automatically repaint exposed areas when windows close, leading to visual artifacts (old window pixels remaining on screen).

Tasks

  • Add picom launch to gar-session.sh (before gar starts)
  • Add picom launch to start-gar.sh xinitrc
  • Keep clear_root_area() as fallback for compositor-less setups
  • Add _NET_WM_BYPASS_COMPOSITOR atom (for fullscreen apps to request un-redirection)

Session Script Changes

# Launch compositor before WM
picom -b --use-ewmh-active-win &
sleep 0.1  # Brief pause to let compositor initialize
  • -b or --daemon: Run as background daemon
  • --use-ewmh-active-win: Use _NET_ACTIVE_WINDOW for focus (more reliable with tiling WMs)
  • --backend glx: Better for screen tearing prevention (optional)

EWMH Atoms for Compositors

Atom Purpose Who Sets It
_NET_WM_BYPASS_COMPOSITOR Un-redirect fullscreen windows Apps (games, videos)
_NET_ACTIVE_WINDOW Currently focused window WM (gar)
_NET_WM_OPACITY Window opacity hint Apps or user tools

Fallback Behavior

The clear_root_area() function remains as a fallback for users who don't have picom installed. When a compositor is running, it handles repainting automatically and clear_root_area() becomes a no-op in effect.

Acceptance Criteria

  1. Windows close without visual artifacts when picom is running
  2. No screen tearing during window operations
  3. Fallback still works when picom is not installed (uses clear_root_area())
  4. Fullscreen apps (games, videos) can bypass compositor via _NET_WM_BYPASS_COMPOSITOR

Keybind Summary

Keybind Action
Mod+F Toggle fullscreen

Acceptance Criteria

  1. Polybar shows workspaces correctly
  2. Rofi can list and switch windows
  3. Dunst notifications work properly
  4. Applications can request fullscreen
  5. Borders and gaps configurable and working
  6. Urgent windows highlighted
  7. No errors from EWMH-compliant applications

Testing Strategy

# Polybar
# Install polybar, configure xworkspaces module
# Workspaces should display, clicking should switch

# Rofi
rofi -show window  # Should list all windows
# Selecting window should focus it

# Fullscreen
# Open Firefox, press F11
# Should go fullscreen correctly
# Press F11 again, should restore

# Urgency
# Set urgent hint on a window
# Border should change, workspace should indicate

Notes

View source
1 # Sprint 8: EWMH Compliance + Polish
2
3 **Goal:** Full EWMH compliance for ecosystem integration, plus visual polish.
4
5 ## Objectives
6
7 - Complete EWMH atom support for tools like polybar, rofi, dunst
8 - Proper ICCCM compliance
9 - Visual features: borders, gaps, optional title bars
10 - Urgency hints
11
12 ## Prerequisites
13
14 - Sprint 7 complete (multi-monitor)
15
16 ## EWMH Compliance
17
18 ### Required Atoms
19
20 | Atom | Purpose | Implementation |
21 |------|---------|----------------|
22 | `_NET_SUPPORTED` | List of supported atoms | Set on root |
23 | `_NET_SUPPORTING_WM_CHECK` | WM identification | Child window |
24 | `_NET_CLIENT_LIST` | All managed windows | Update on changes |
25 | `_NET_CLIENT_LIST_STACKING` | Windows in stacking order | Update on changes |
26 | `_NET_NUMBER_OF_DESKTOPS` | Workspace count | Set on init |
27 | `_NET_DESKTOP_GEOMETRY` | Virtual desktop size | Set on init |
28 | `_NET_DESKTOP_VIEWPORT` | Viewport position | Per workspace |
29 | `_NET_CURRENT_DESKTOP` | Active workspace | Update on switch |
30 | `_NET_DESKTOP_NAMES` | Workspace names | Set on init |
31 | `_NET_ACTIVE_WINDOW` | Focused window | Update on focus |
32 | `_NET_WORKAREA` | Usable screen area | Per monitor |
33 | `_NET_WM_NAME` | WM name | "gar" |
34 | `_NET_WM_DESKTOP` | Window's workspace | Per window |
35 | `_NET_WM_STATE` | Window states | Per window |
36 | `_NET_WM_WINDOW_TYPE` | Window type | Read, respect |
37
38 ## Tasks
39
40 ### 8.1 EWMH Setup
41 - [ ] Create `src/x11/ewmh.rs`
42 - [ ] Intern all EWMH atoms on startup
43 - [ ] Create supporting WM check window
44 - [ ] Set `_NET_SUPPORTED` with all supported atoms
45 - [ ] Set WM name
46
47 ```rust
48 pub struct EwmhAtoms {
49 pub _NET_SUPPORTED: Atom,
50 pub _NET_SUPPORTING_WM_CHECK: Atom,
51 pub _NET_CLIENT_LIST: Atom,
52 pub _NET_CLIENT_LIST_STACKING: Atom,
53 pub _NET_NUMBER_OF_DESKTOPS: Atom,
54 pub _NET_CURRENT_DESKTOP: Atom,
55 pub _NET_DESKTOP_NAMES: Atom,
56 pub _NET_ACTIVE_WINDOW: Atom,
57 pub _NET_WM_NAME: Atom,
58 pub _NET_WM_DESKTOP: Atom,
59 pub _NET_WM_STATE: Atom,
60 pub _NET_WM_STATE_FULLSCREEN: Atom,
61 pub _NET_WM_STATE_HIDDEN: Atom,
62 pub _NET_WM_STATE_DEMANDS_ATTENTION: Atom,
63 pub _NET_WM_WINDOW_TYPE: Atom,
64 pub _NET_WM_WINDOW_TYPE_DIALOG: Atom,
65 // ... more
66 }
67
68 fn setup_ewmh(conn: &impl Connection, root: Window, atoms: &EwmhAtoms) -> Result<()> {
69 // Create check window
70 let check_window = conn.generate_id()?;
71 conn.create_window(
72 0, check_window, root,
73 -1, -1, 1, 1, 0,
74 WindowClass::INPUT_OUTPUT,
75 0,
76 &CreateWindowAux::new(),
77 )?;
78
79 // Set supporting WM check
80 conn.change_property32(
81 PropMode::REPLACE, root,
82 atoms._NET_SUPPORTING_WM_CHECK,
83 AtomEnum::WINDOW,
84 &[check_window],
85 )?;
86 conn.change_property32(
87 PropMode::REPLACE, check_window,
88 atoms._NET_SUPPORTING_WM_CHECK,
89 AtomEnum::WINDOW,
90 &[check_window],
91 )?;
92
93 // Set WM name
94 conn.change_property8(
95 PropMode::REPLACE, check_window,
96 atoms._NET_WM_NAME,
97 atoms.UTF8_STRING,
98 b"gar",
99 )?;
100
101 // Set supported atoms
102 let supported = vec![
103 atoms._NET_SUPPORTED,
104 atoms._NET_SUPPORTING_WM_CHECK,
105 // ... all supported atoms
106 ];
107 conn.change_property32(
108 PropMode::REPLACE, root,
109 atoms._NET_SUPPORTED,
110 AtomEnum::ATOM,
111 &supported,
112 )?;
113
114 Ok(())
115 }
116 ```
117
118 ### 8.2 Client List Maintenance
119 - [ ] Update `_NET_CLIENT_LIST` on window add/remove
120 - [ ] Update `_NET_CLIENT_LIST_STACKING` on stacking changes
121 - [ ] Update `_NET_ACTIVE_WINDOW` on focus change
122
123 ```rust
124 fn update_client_list(&self, conn: &impl Connection) -> Result<()> {
125 let windows: Vec<u32> = self.all_windows()
126 .iter()
127 .map(|w| w.id)
128 .collect();
129
130 conn.change_property32(
131 PropMode::REPLACE,
132 self.root,
133 self.atoms._NET_CLIENT_LIST,
134 AtomEnum::WINDOW,
135 &windows,
136 )?;
137
138 Ok(())
139 }
140 ```
141
142 ### 8.3 Handle Client Messages
143 - [ ] Handle `_NET_ACTIVE_WINDOW` (focus requests from apps)
144 - [ ] Handle `_NET_WM_STATE` changes (fullscreen, etc.)
145 - [ ] Handle `_NET_CURRENT_DESKTOP` (pager workspace switch)
146 - [ ] Handle `_NET_CLOSE_WINDOW` (close requests)
147
148 ```rust
149 fn handle_client_message(&mut self, event: ClientMessageEvent) -> Result<()> {
150 let atom = event.type_;
151
152 if atom == self.atoms._NET_ACTIVE_WINDOW {
153 // Application requesting focus
154 let window = event.window;
155 self.focus_window(window)?;
156 } else if atom == self.atoms._NET_WM_STATE {
157 // State change request (e.g., fullscreen)
158 let action = event.data.as_data32()[0];
159 let prop = event.data.as_data32()[1];
160 self.handle_state_change(event.window, action, prop)?;
161 } else if atom == self.atoms._NET_CURRENT_DESKTOP {
162 // Workspace switch request
163 let desktop = event.data.as_data32()[0] as usize;
164 self.switch_workspace(WorkspaceId(desktop + 1))?;
165 }
166
167 Ok(())
168 }
169 ```
170
171 ### 8.4 Window State Management
172 - [ ] Track window states (fullscreen, hidden, urgent)
173 - [ ] Update `_NET_WM_STATE` property
174 - [ ] Implement fullscreen toggle (Mod+F)
175 - [ ] Handle fullscreen requests from applications
176
177 ```rust
178 fn set_fullscreen(&mut self, window: WindowId, fullscreen: bool) -> Result<()> {
179 let win = self.get_window_mut(window)?;
180
181 if fullscreen {
182 // Save current geometry
183 win.saved_geometry = Some(win.geometry);
184 // Set to monitor size
185 let monitor = self.get_monitor_for_window(window);
186 win.geometry = monitor.geometry;
187 win.states.insert(WindowState::Fullscreen);
188 } else {
189 // Restore geometry
190 if let Some(saved) = win.saved_geometry.take() {
191 win.geometry = saved;
192 }
193 win.states.remove(&WindowState::Fullscreen);
194 }
195
196 // Update X property
197 self.update_wm_state(window)?;
198 self.apply_geometry(window)?;
199
200 Ok(())
201 }
202 ```
203
204 ### 8.5 ICCCM Compliance
205 - [ ] Create `src/x11/icccm.rs`
206 - [ ] Handle `WM_HINTS` (urgency, input, etc.)
207 - [ ] Handle `WM_NORMAL_HINTS` (size hints)
208 - [ ] Handle `WM_PROTOCOLS` (delete window, take focus)
209 - [ ] Handle `WM_TRANSIENT_FOR`
210
211 ```rust
212 fn get_wm_hints(conn: &impl Connection, window: Window) -> Result<Option<WmHints>> {
213 // Read WM_HINTS property
214 // Parse into struct
215 }
216
217 fn get_size_hints(conn: &impl Connection, window: Window) -> Result<Option<SizeHints>> {
218 // Read WM_NORMAL_HINTS
219 // Extract min/max size, aspect ratio, etc.
220 }
221 ```
222
223 ### 8.6 Border Colors
224 - [ ] Configurable border colors via Lua
225 - [ ] Different colors: focused, unfocused, urgent
226 - [ ] Apply colors on focus change
227 - [ ] Flash urgent windows
228
229 ```rust
230 fn update_border_color(&self, conn: &impl Connection, window: WindowId) -> Result<()> {
231 let win = self.get_window(window)?;
232 let color = if win.urgent {
233 self.config.border_color_urgent
234 } else if Some(window) == self.focused {
235 self.config.border_color_focused
236 } else {
237 self.config.border_color_unfocused
238 };
239
240 conn.change_window_attributes(
241 window,
242 &ChangeWindowAttributesAux::new().border_pixel(color),
243 )?;
244
245 Ok(())
246 }
247 ```
248
249 ### 8.7 Gaps
250 - [ ] Inner gaps (between windows)
251 - [ ] Outer gaps (screen edge)
252 - [ ] Configurable via Lua
253 - [ ] Apply in geometry calculation
254
255 ```rust
256 fn calculate_geometries_with_gaps(&self, rect: Rect, gaps: &GapConfig) -> Vec<(WindowId, Rect)> {
257 // Shrink rect by outer gaps
258 let work_area = Rect {
259 x: rect.x + gaps.outer as i16,
260 y: rect.y + gaps.outer as i16,
261 width: rect.width - 2 * gaps.outer,
262 height: rect.height - 2 * gaps.outer,
263 };
264
265 // Calculate from tree
266 let mut geometries = self.tree.calculate_geometries(work_area);
267
268 // Apply inner gaps
269 for (_, geom) in &mut geometries {
270 geom.x += gaps.inner as i16 / 2;
271 geom.y += gaps.inner as i16 / 2;
272 geom.width -= gaps.inner;
273 geom.height -= gaps.inner;
274 }
275
276 geometries
277 }
278 ```
279
280 ### 8.8 Optional Title Bars
281 - [ ] Create title bar windows (reparenting)
282 - [ ] Draw window title
283 - [ ] Handle title bar clicks (focus, move)
284 - [ ] Configurable per window class
285 - [ ] Double-click to toggle maximize
286
287 ```rust
288 fn create_frame(&mut self, conn: &impl Connection, window: WindowId) -> Result<Frame> {
289 let title_height = if self.should_have_titlebar(window) {
290 self.config.titlebar_height
291 } else {
292 0
293 };
294
295 // Create frame window
296 let frame = conn.generate_id()?;
297 conn.create_window(/* ... */)?;
298
299 // Reparent client into frame
300 conn.reparent_window(window, frame, 0, title_height)?;
301
302 Ok(Frame { id: frame, client: window, title_height })
303 }
304 ```
305
306 ### 8.9 Urgency Hints
307 - [ ] Read `WM_HINTS` urgency flag
308 - [ ] Read `_NET_WM_STATE_DEMANDS_ATTENTION`
309 - [ ] Set urgent border color
310 - [ ] Indicate urgent workspace (for status bars)
311 - [ ] Clear urgency on focus
312
313 ```rust
314 fn handle_urgency(&mut self, window: WindowId) -> Result<()> {
315 let hints = get_wm_hints(&self.conn, window)?;
316
317 if hints.map(|h| h.urgent).unwrap_or(false) {
318 let win = self.get_window_mut(window)?;
319 win.urgent = true;
320
321 // Update workspace urgency
322 let ws = self.get_workspace_for_window(window)?;
323 self.workspaces[ws].urgent = true;
324
325 // Update EWMH
326 self.set_wm_state(window, StateAction::Add, self.atoms._NET_WM_STATE_DEMANDS_ATTENTION)?;
327
328 // Update border
329 self.update_border_color(window)?;
330 }
331
332 Ok(())
333 }
334 ```
335
336 ### 8.10 External Compositor Integration
337
338 **Goal:** Support external compositors (picom) for proper screen repainting and visual effects.
339
340 #### Background
341
342 Like i3, gar is NOT a compositing window manager. We rely on external compositors (picom, compton, xcompmgr) for:
343 - Proper screen repainting when windows close
344 - Transparency and visual effects
345 - Vsync and tear-free rendering
346
347 Without a compositor, X11 does not automatically repaint exposed areas when windows close, leading to visual artifacts (old window pixels remaining on screen).
348
349 #### Tasks
350 - [ ] Add picom launch to `gar-session.sh` (before gar starts)
351 - [ ] Add picom launch to `start-gar.sh` xinitrc
352 - [ ] Keep `clear_root_area()` as fallback for compositor-less setups
353 - [ ] Add `_NET_WM_BYPASS_COMPOSITOR` atom (for fullscreen apps to request un-redirection)
354
355 #### Session Script Changes
356 ```bash
357 # Launch compositor before WM
358 picom -b --use-ewmh-active-win &
359 sleep 0.1 # Brief pause to let compositor initialize
360 ```
361
362 #### Recommended Picom Flags
363 - `-b` or `--daemon`: Run as background daemon
364 - `--use-ewmh-active-win`: Use `_NET_ACTIVE_WINDOW` for focus (more reliable with tiling WMs)
365 - `--backend glx`: Better for screen tearing prevention (optional)
366
367 #### EWMH Atoms for Compositors
368 | Atom | Purpose | Who Sets It |
369 |------|---------|-------------|
370 | `_NET_WM_BYPASS_COMPOSITOR` | Un-redirect fullscreen windows | Apps (games, videos) |
371 | `_NET_ACTIVE_WINDOW` | Currently focused window | WM (gar) |
372 | `_NET_WM_OPACITY` | Window opacity hint | Apps or user tools |
373
374 #### Fallback Behavior
375 The `clear_root_area()` function remains as a fallback for users who don't have picom installed. When a compositor is running, it handles repainting automatically and `clear_root_area()` becomes a no-op in effect.
376
377 #### Acceptance Criteria
378 1. Windows close without visual artifacts when picom is running
379 2. No screen tearing during window operations
380 3. Fallback still works when picom is not installed (uses `clear_root_area()`)
381 4. Fullscreen apps (games, videos) can bypass compositor via `_NET_WM_BYPASS_COMPOSITOR`
382
383 ---
384
385 ## Keybind Summary
386
387 | Keybind | Action |
388 |---------|--------|
389 | Mod+F | Toggle fullscreen |
390
391 ## Acceptance Criteria
392
393 1. Polybar shows workspaces correctly
394 2. Rofi can list and switch windows
395 3. Dunst notifications work properly
396 4. Applications can request fullscreen
397 5. Borders and gaps configurable and working
398 6. Urgent windows highlighted
399 7. No errors from EWMH-compliant applications
400
401 ## Testing Strategy
402
403 ```bash
404 # Polybar
405 # Install polybar, configure xworkspaces module
406 # Workspaces should display, clicking should switch
407
408 # Rofi
409 rofi -show window # Should list all windows
410 # Selecting window should focus it
411
412 # Fullscreen
413 # Open Firefox, press F11
414 # Should go fullscreen correctly
415 # Press F11 again, should restore
416
417 # Urgency
418 # Set urgent hint on a window
419 # Border should change, workspace should indicate
420 ```
421
422 ## Notes
423
424 - EWMH spec: https://specifications.freedesktop.org/wm-spec/latest/
425 - ICCCM spec: https://tronche.com/gui/x/icccm/
426 - Title bars add complexity - consider making opt-in
427 - Test with various applications (Firefox, Chromium, mpv, etc.)