markdown · 7776 bytes Raw Blame History

Sprint 9: MVP Polish + Testing

Goal: Stable, documented MVP ready for release.

Objectives

  • Robust error handling throughout
  • Crash recovery (restart in place)
  • Documentation (man pages, examples)
  • Integration testing
  • Installation packaging

Prerequisites

  • Sprint 8 complete (EWMH, polish)

Tasks

9.1 Error Handling Audit

  • Review all unwrap() and expect() calls
  • Replace with proper error handling
  • Add context to errors with thiserror
  • Log errors appropriately
  • Never crash on X errors (use error handler)
// Bad
let window = self.get_window(id).unwrap();

// Good
let window = self.get_window(id)
    .ok_or_else(|| Error::WindowNotFound(id))?;

// With context
let tree = std::fs::read_to_string(&config_path)
    .map_err(|e| Error::ConfigLoad { path: config_path.clone(), source: e })?;

9.2 X Error Handling

  • Set up X error handler
  • Log X errors without crashing
  • Handle "window destroyed" race conditions
  • Recover from non-fatal errors
fn setup_error_handler(conn: &impl Connection) {
    // X11 error handling is synchronous in x11rb
    // Wrap operations that might fail due to window destruction
}

fn safe_configure_window(
    conn: &impl Connection,
    window: Window,
    aux: &ConfigureWindowAux,
) -> Result<()> {
    match conn.configure_window(window, aux)?.check() {
        Ok(_) => Ok(()),
        Err(e) if is_window_error(&e) => {
            tracing::debug!("Window {} no longer exists", window);
            Ok(()) // Not a fatal error
        }
        Err(e) => Err(e.into()),
    }
}

9.3 Crash Recovery

  • Implement --replace flag
  • Save state before exit/crash
  • Restore state on restart
  • Handle SIGTERM/SIGINT gracefully
  • Support restart-in-place (Mod+Shift+R alternative)
fn save_state(&self, path: &Path) -> Result<()> {
    let state = SavedState {
        workspaces: self.workspaces.iter().map(|ws| {
            SavedWorkspace {
                name: ws.name.clone(),
                windows: ws.all_windows(),
            }
        }).collect(),
        focused: self.focused,
    };
    let json = serde_json::to_string(&state)?;
    std::fs::write(path, json)?;
    Ok(())
}

fn restore_state(&mut self, path: &Path) -> Result<()> {
    let json = std::fs::read_to_string(path)?;
    let state: SavedState = serde_json::from_str(&json)?;
    // Restore workspaces and focus
    Ok(())
}

fn handle_signal(sig: i32) {
    match sig {
        SIGTERM | SIGINT => {
            // Save state and exit cleanly
            save_state(&state_path).ok();
            std::process::exit(0);
        }
        SIGUSR1 => {
            // Restart in place
            save_state(&state_path).ok();
            exec_self();
        }
        _ => {}
    }
}

9.4 Man Pages

  • Create docs/gar.1 (main man page)
  • Create docs/garctl.1 (CLI tool)
  • Create docs/gar-lua.5 (configuration)
  • Document all keybinds and options
.TH GAR 1 "2024" "gar" "User Commands"
.SH NAME
gar \- tiling window manager with smart splits
.SH SYNOPSIS
.B gar
[\fIOPTIONS\fR]
.SH DESCRIPTION
.B gar
is an X11 tiling window manager that automatically determines
the optimal split direction when creating new windows.
.SH OPTIONS
.TP
.B \-c, \-\-config FILE
Use alternate configuration file
.TP
.B \-\-replace
Replace currently running window manager
.TP
.B \-v, \-\-version
Print version and exit
.SH FILES
.TP
.I ~/.config/gar/init.lua
Default configuration file
.TP
.I $XDG_RUNTIME_DIR/gar.sock
IPC socket
.SH SEE ALSO
.BR garctl (1),
.BR gar-lua (5)

9.5 Example Configurations

  • Create examples/minimal.lua
  • Create examples/i3-like.lua
  • Create examples/gaps-and-borders.lua
  • Create examples/polybar-integration.lua
  • Document each example

9.6 Integration Tests

  • Set up test infrastructure with Xvfb
  • Test window creation/destruction
  • Test workspace switching
  • Test keybind execution
  • Test IPC commands
  • Test config loading
// tests/integration.rs
use std::process::Command;

fn setup_xvfb() -> XvfbGuard {
    // Start Xvfb on :99
    // Return guard that kills on drop
}

#[test]
fn test_window_tiling() {
    let _xvfb = setup_xvfb();

    // Start gar
    let mut gar = Command::new("cargo")
        .args(["run", "--"])
        .env("DISPLAY", ":99")
        .spawn()
        .unwrap();

    // Wait for startup
    std::thread::sleep(Duration::from_millis(500));

    // Open windows
    Command::new("xterm")
        .env("DISPLAY", ":99")
        .spawn()
        .unwrap();

    // Verify via IPC
    let output = Command::new("garctl")
        .args(["get-tree"])
        .env("DISPLAY", ":99")
        .output()
        .unwrap();

    let tree: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    // Assert tree structure

    gar.kill().unwrap();
}

9.7 Installation Script

  • Create install.sh for manual install
  • Create Makefile with install target
  • Document dependencies
  • Test on fresh system
#!/bin/bash
# install.sh

set -e

PREFIX=${PREFIX:-/usr/local}

cargo build --release

install -Dm755 target/release/gar "$PREFIX/bin/gar"
install -Dm755 target/release/garctl "$PREFIX/bin/garctl"
install -Dm644 gar.desktop "$PREFIX/share/xsessions/gar.desktop"
install -Dm644 docs/gar.1 "$PREFIX/share/man/man1/gar.1"
install -Dm644 docs/garctl.1 "$PREFIX/share/man/man1/garctl.1"

echo "Installation complete!"

9.8 AUR Package (optional)

  • Create PKGBUILD
  • Test in clean chroot
  • Submit to AUR
# PKGBUILD
pkgname=gar
pkgver=0.1.0
pkgrel=1
pkgdesc="Tiling window manager with smart splits"
arch=('x86_64')
url="https://github.com/youruser/gar"
license=('MIT')
depends=('libxcb' 'lua')
makedepends=('rust' 'cargo')

build() {
    cd "$srcdir/$pkgname-$pkgver"
    cargo build --release
}

package() {
    cd "$srcdir/$pkgname-$pkgver"
    install -Dm755 target/release/gar "$pkgdir/usr/bin/gar"
    install -Dm755 target/release/garctl "$pkgdir/usr/bin/garctl"
    install -Dm644 gar.desktop "$pkgdir/usr/share/xsessions/gar.desktop"
}

9.9 README and Documentation

  • Update README with features, screenshots
  • Add CONTRIBUTING.md
  • Add LICENSE file
  • Document known issues/limitations

9.10 Final Testing Checklist

  • Fresh install works
  • All documented keybinds work
  • Polybar integration works
  • Rofi integration works
  • Multi-monitor works
  • Config reload works
  • No memory leaks (valgrind)
  • No crash on stress test

Acceptance Criteria

  1. No panics in release build under normal use
  2. Man pages installed and accessible
  3. Example configs provided and documented
  4. Integration tests pass
  5. Install script works on fresh Arch/Ubuntu
  6. README provides clear getting started guide

Testing Strategy

# Full test suite
cargo test

# Integration tests (requires Xvfb)
cargo test --test integration

# Memory check
valgrind --leak-check=full target/release/gar

# Stress test
for i in {1..100}; do
    DISPLAY=:1 xterm &
done
# Then close all, verify no issues

MVP Feature Summary

After Sprint 9, gar will have:

  • Smart split detection (the core feature)
  • Full keyboard navigation
  • 10 workspaces
  • Multi-monitor support
  • Floating window support
  • Lua configuration
  • IPC system with garctl
  • EWMH compliance
  • Configurable borders and gaps
  • Documentation and examples

Post-MVP Ideas (Future Sprints)

  • Scratchpad windows
  • Tabbed/stacked layouts
  • Animations (fade, slide)
  • Built-in bar (optional)
  • Session save/restore
  • Marks (like vim marks)
  • Modes (resize mode, move mode)
  • More layout algorithms
View source
1 # Sprint 9: MVP Polish + Testing
2
3 **Goal:** Stable, documented MVP ready for release.
4
5 ## Objectives
6
7 - Robust error handling throughout
8 - Crash recovery (restart in place)
9 - Documentation (man pages, examples)
10 - Integration testing
11 - Installation packaging
12
13 ## Prerequisites
14
15 - Sprint 8 complete (EWMH, polish)
16
17 ## Tasks
18
19 ### 9.1 Error Handling Audit
20 - [ ] Review all `unwrap()` and `expect()` calls
21 - [ ] Replace with proper error handling
22 - [ ] Add context to errors with `thiserror`
23 - [ ] Log errors appropriately
24 - [ ] Never crash on X errors (use error handler)
25
26 ```rust
27 // Bad
28 let window = self.get_window(id).unwrap();
29
30 // Good
31 let window = self.get_window(id)
32 .ok_or_else(|| Error::WindowNotFound(id))?;
33
34 // With context
35 let tree = std::fs::read_to_string(&config_path)
36 .map_err(|e| Error::ConfigLoad { path: config_path.clone(), source: e })?;
37 ```
38
39 ### 9.2 X Error Handling
40 - [ ] Set up X error handler
41 - [ ] Log X errors without crashing
42 - [ ] Handle "window destroyed" race conditions
43 - [ ] Recover from non-fatal errors
44
45 ```rust
46 fn setup_error_handler(conn: &impl Connection) {
47 // X11 error handling is synchronous in x11rb
48 // Wrap operations that might fail due to window destruction
49 }
50
51 fn safe_configure_window(
52 conn: &impl Connection,
53 window: Window,
54 aux: &ConfigureWindowAux,
55 ) -> Result<()> {
56 match conn.configure_window(window, aux)?.check() {
57 Ok(_) => Ok(()),
58 Err(e) if is_window_error(&e) => {
59 tracing::debug!("Window {} no longer exists", window);
60 Ok(()) // Not a fatal error
61 }
62 Err(e) => Err(e.into()),
63 }
64 }
65 ```
66
67 ### 9.3 Crash Recovery
68 - [ ] Implement `--replace` flag
69 - [ ] Save state before exit/crash
70 - [ ] Restore state on restart
71 - [ ] Handle SIGTERM/SIGINT gracefully
72 - [ ] Support restart-in-place (Mod+Shift+R alternative)
73
74 ```rust
75 fn save_state(&self, path: &Path) -> Result<()> {
76 let state = SavedState {
77 workspaces: self.workspaces.iter().map(|ws| {
78 SavedWorkspace {
79 name: ws.name.clone(),
80 windows: ws.all_windows(),
81 }
82 }).collect(),
83 focused: self.focused,
84 };
85 let json = serde_json::to_string(&state)?;
86 std::fs::write(path, json)?;
87 Ok(())
88 }
89
90 fn restore_state(&mut self, path: &Path) -> Result<()> {
91 let json = std::fs::read_to_string(path)?;
92 let state: SavedState = serde_json::from_str(&json)?;
93 // Restore workspaces and focus
94 Ok(())
95 }
96
97 fn handle_signal(sig: i32) {
98 match sig {
99 SIGTERM | SIGINT => {
100 // Save state and exit cleanly
101 save_state(&state_path).ok();
102 std::process::exit(0);
103 }
104 SIGUSR1 => {
105 // Restart in place
106 save_state(&state_path).ok();
107 exec_self();
108 }
109 _ => {}
110 }
111 }
112 ```
113
114 ### 9.4 Man Pages
115 - [ ] Create `docs/gar.1` (main man page)
116 - [ ] Create `docs/garctl.1` (CLI tool)
117 - [ ] Create `docs/gar-lua.5` (configuration)
118 - [ ] Document all keybinds and options
119
120 ```man
121 .TH GAR 1 "2024" "gar" "User Commands"
122 .SH NAME
123 gar \- tiling window manager with smart splits
124 .SH SYNOPSIS
125 .B gar
126 [\fIOPTIONS\fR]
127 .SH DESCRIPTION
128 .B gar
129 is an X11 tiling window manager that automatically determines
130 the optimal split direction when creating new windows.
131 .SH OPTIONS
132 .TP
133 .B \-c, \-\-config FILE
134 Use alternate configuration file
135 .TP
136 .B \-\-replace
137 Replace currently running window manager
138 .TP
139 .B \-v, \-\-version
140 Print version and exit
141 .SH FILES
142 .TP
143 .I ~/.config/gar/init.lua
144 Default configuration file
145 .TP
146 .I $XDG_RUNTIME_DIR/gar.sock
147 IPC socket
148 .SH SEE ALSO
149 .BR garctl (1),
150 .BR gar-lua (5)
151 ```
152
153 ### 9.5 Example Configurations
154 - [ ] Create `examples/minimal.lua`
155 - [ ] Create `examples/i3-like.lua`
156 - [ ] Create `examples/gaps-and-borders.lua`
157 - [ ] Create `examples/polybar-integration.lua`
158 - [ ] Document each example
159
160 ### 9.6 Integration Tests
161 - [ ] Set up test infrastructure with Xvfb
162 - [ ] Test window creation/destruction
163 - [ ] Test workspace switching
164 - [ ] Test keybind execution
165 - [ ] Test IPC commands
166 - [ ] Test config loading
167
168 ```rust
169 // tests/integration.rs
170 use std::process::Command;
171
172 fn setup_xvfb() -> XvfbGuard {
173 // Start Xvfb on :99
174 // Return guard that kills on drop
175 }
176
177 #[test]
178 fn test_window_tiling() {
179 let _xvfb = setup_xvfb();
180
181 // Start gar
182 let mut gar = Command::new("cargo")
183 .args(["run", "--"])
184 .env("DISPLAY", ":99")
185 .spawn()
186 .unwrap();
187
188 // Wait for startup
189 std::thread::sleep(Duration::from_millis(500));
190
191 // Open windows
192 Command::new("xterm")
193 .env("DISPLAY", ":99")
194 .spawn()
195 .unwrap();
196
197 // Verify via IPC
198 let output = Command::new("garctl")
199 .args(["get-tree"])
200 .env("DISPLAY", ":99")
201 .output()
202 .unwrap();
203
204 let tree: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
205 // Assert tree structure
206
207 gar.kill().unwrap();
208 }
209 ```
210
211 ### 9.7 Installation Script
212 - [ ] Create `install.sh` for manual install
213 - [ ] Create Makefile with install target
214 - [ ] Document dependencies
215 - [ ] Test on fresh system
216
217 ```bash
218 #!/bin/bash
219 # install.sh
220
221 set -e
222
223 PREFIX=${PREFIX:-/usr/local}
224
225 cargo build --release
226
227 install -Dm755 target/release/gar "$PREFIX/bin/gar"
228 install -Dm755 target/release/garctl "$PREFIX/bin/garctl"
229 install -Dm644 gar.desktop "$PREFIX/share/xsessions/gar.desktop"
230 install -Dm644 docs/gar.1 "$PREFIX/share/man/man1/gar.1"
231 install -Dm644 docs/garctl.1 "$PREFIX/share/man/man1/garctl.1"
232
233 echo "Installation complete!"
234 ```
235
236 ### 9.8 AUR Package (optional)
237 - [ ] Create PKGBUILD
238 - [ ] Test in clean chroot
239 - [ ] Submit to AUR
240
241 ```bash
242 # PKGBUILD
243 pkgname=gar
244 pkgver=0.1.0
245 pkgrel=1
246 pkgdesc="Tiling window manager with smart splits"
247 arch=('x86_64')
248 url="https://github.com/youruser/gar"
249 license=('MIT')
250 depends=('libxcb' 'lua')
251 makedepends=('rust' 'cargo')
252
253 build() {
254 cd "$srcdir/$pkgname-$pkgver"
255 cargo build --release
256 }
257
258 package() {
259 cd "$srcdir/$pkgname-$pkgver"
260 install -Dm755 target/release/gar "$pkgdir/usr/bin/gar"
261 install -Dm755 target/release/garctl "$pkgdir/usr/bin/garctl"
262 install -Dm644 gar.desktop "$pkgdir/usr/share/xsessions/gar.desktop"
263 }
264 ```
265
266 ### 9.9 README and Documentation
267 - [ ] Update README with features, screenshots
268 - [ ] Add CONTRIBUTING.md
269 - [ ] Add LICENSE file
270 - [ ] Document known issues/limitations
271
272 ### 9.10 Final Testing Checklist
273 - [ ] Fresh install works
274 - [ ] All documented keybinds work
275 - [ ] Polybar integration works
276 - [ ] Rofi integration works
277 - [ ] Multi-monitor works
278 - [ ] Config reload works
279 - [ ] No memory leaks (valgrind)
280 - [ ] No crash on stress test
281
282 ## Acceptance Criteria
283
284 1. No panics in release build under normal use
285 2. Man pages installed and accessible
286 3. Example configs provided and documented
287 4. Integration tests pass
288 5. Install script works on fresh Arch/Ubuntu
289 6. README provides clear getting started guide
290
291 ## Testing Strategy
292
293 ```bash
294 # Full test suite
295 cargo test
296
297 # Integration tests (requires Xvfb)
298 cargo test --test integration
299
300 # Memory check
301 valgrind --leak-check=full target/release/gar
302
303 # Stress test
304 for i in {1..100}; do
305 DISPLAY=:1 xterm &
306 done
307 # Then close all, verify no issues
308 ```
309
310 ## MVP Feature Summary
311
312 After Sprint 9, gar will have:
313
314 - Smart split detection (the core feature)
315 - Full keyboard navigation
316 - 10 workspaces
317 - Multi-monitor support
318 - Floating window support
319 - Lua configuration
320 - IPC system with garctl
321 - EWMH compliance
322 - Configurable borders and gaps
323 - Documentation and examples
324
325 ## Post-MVP Ideas (Future Sprints)
326
327 - Scratchpad windows
328 - Tabbed/stacked layouts
329 - Animations (fade, slide)
330 - Built-in bar (optional)
331 - Session save/restore
332 - Marks (like vim marks)
333 - Modes (resize mode, move mode)
334 - More layout algorithms