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_SUPPORTEDwith 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_LISTon window add/remove - Update
_NET_CLIENT_LIST_STACKINGon stacking changes - Update
_NET_ACTIVE_WINDOWon 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_STATEchanges (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_STATEproperty - 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_HINTSurgency 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.shxinitrc - Keep
clear_root_area()as fallback for compositor-less setups - Add
_NET_WM_BYPASS_COMPOSITORatom (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
Recommended Picom Flags
-bor--daemon: Run as background daemon--use-ewmh-active-win: Use_NET_ACTIVE_WINDOWfor 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
- Windows close without visual artifacts when picom is running
- No screen tearing during window operations
- Fallback still works when picom is not installed (uses
clear_root_area()) - Fullscreen apps (games, videos) can bypass compositor via
_NET_WM_BYPASS_COMPOSITOR
Keybind Summary
| Keybind | Action |
|---|---|
| Mod+F | Toggle fullscreen |
Acceptance Criteria
- Polybar shows workspaces correctly
- Rofi can list and switch windows
- Dunst notifications work properly
- Applications can request fullscreen
- Borders and gaps configurable and working
- Urgent windows highlighted
- 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
- EWMH spec: https://specifications.freedesktop.org/wm-spec/latest/
- ICCCM spec: https://tronche.com/gui/x/icccm/
- Title bars add complexity - consider making opt-in
- Test with various applications (Firefox, Chromium, mpv, etc.)
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.) |