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()andexpect()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
--replaceflag - 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.shfor 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
- No panics in release build under normal use
- Man pages installed and accessible
- Example configs provided and documented
- Integration tests pass
- Install script works on fresh Arch/Ubuntu
- 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 |