Sprint 2: Smart Split + Keyboard Navigation
Goal: Implement the core differentiating feature (intelligent split direction) and full keyboard-based window navigation.
Objectives
- Automatic split direction based on window dimensions
- Full keyboard navigation (vim-style hjkl)
- Keyboard-based window operations
- This is what makes gar different from i3
Prerequisites
- Sprint 1 complete (BSP tree, basic window management)
The Smart Split Algorithm
This is the key feature that Hyprland has and i3 lacks:
When inserting a new window:
1. Get dimensions of the target area (width, height)
2. If width > height → split Vertical (creates side-by-side windows)
3. If height >= width → split Horizontal (creates stacked windows)
Visual example:
┌─────────────────────────┐ ┌────────────┬────────────┐
│ │ │ │ │
│ Wide container │ --> │ Left │ Right │
│ (width > height) │ │ │ (new) │
│ │ │ │ │
└─────────────────────────┘ └────────────┴────────────┘
Split VERTICAL
┌───────────┐ ┌───────────┐
│ │ │ Top │
│ Tall │ ├───────────┤
│ container │ ────────> │ Bottom │
│ (h >= w) │ │ (new) │
│ │ │ │
└───────────┘ └───────────┘
Split HORIZONTAL
Tasks
2.1 Implement Smart Split
- Modify
insert_windowin tree.rs - Calculate container dimensions before inserting
- Choose split direction based on width vs height
- Add unit tests for split direction logic
impl Node {
fn determine_split_direction(rect: &Rect) -> SplitDirection {
if rect.width > rect.height {
SplitDirection::Vertical
} else {
SplitDirection::Horizontal
}
}
pub fn insert_window(&mut self, window: WindowId, rect: Rect) {
let direction = Self::determine_split_direction(&rect);
// ... rest of insertion logic
}
}
2.2 Keyboard Input Infrastructure
- Create
src/input/mod.rsandsrc/input/keybinds.rs - Define keybind action enum
- Create keybind registry
- Grab all configured keys on startup
- Dispatch to actions on KeyPress event
pub enum Action {
FocusDirection(Direction),
SwapDirection(Direction),
ResizeDirection(Direction, i32),
CloseWindow,
Equalize,
Exec(String),
}
pub enum Direction {
Left, Right, Up, Down,
}
2.3 Focus Navigation
- Implement directional focus (Mod+h/j/k/l)
- Find window in given direction from focused
- Handle edge cases (no window in direction)
- Update focus and visual feedback
Algorithm for finding window in direction:
fn find_window_in_direction(
tree: &Node,
from: WindowId,
direction: Direction,
) -> Option<WindowId> {
// 1. Get geometry of 'from' window
// 2. Get geometries of all windows
// 3. Filter to windows in the given direction
// 4. Return closest window in that direction
}
2.4 Window Closing
- Implement Mod+Shift+Q to close focused window
- Send WM_DELETE_WINDOW if supported (ICCCM)
- Fall back to XKillClient if not
- Remove from tree after close
fn close_window(conn: &impl Connection, window: Window) -> Result<()> {
// Check if window supports WM_DELETE_WINDOW
if supports_delete_window(conn, window)? {
send_delete_window_message(conn, window)?;
} else {
conn.kill_client(window)?;
}
Ok(())
}
2.5 Resize Splits
- Implement Mod+Ctrl+h/j/k/l to resize
- Find the split that affects the focused window in that direction
- Adjust ratio by fixed increment (e.g., 0.05)
- Clamp ratio between 0.1 and 0.9
- Recalculate and apply geometries
fn resize_in_direction(tree: &mut Node, window: WindowId, dir: Direction, delta: f32) {
// Find the nearest ancestor split in the given direction
// Adjust its ratio
}
2.6 Equalize Splits
- Implement Mod+E to equalize
- Set all split ratios to 0.5
- Recursive traversal of tree
- Recalculate and apply geometries
fn equalize(node: &mut Node) {
match node {
Node::Internal { ratio, left, right, .. } => {
*ratio = 0.5;
equalize(left);
equalize(right);
}
Node::Leaf { .. } => {}
}
}
2.7 Swap Windows
- Implement Mod+Shift+h/j/k/l to swap
- Find window in direction
- Swap the two windows in the tree
- Recalculate and apply geometries
fn swap_windows(tree: &mut Node, a: WindowId, b: WindowId) {
// Find both leaf nodes
// Swap their window IDs
}
Keybind Summary
| Keybind | Action |
|---|---|
| Mod+h/j/k/l | Focus left/down/up/right |
| Mod+Shift+h/j/k/l | Swap with left/down/up/right |
| Mod+Ctrl+h/j/k/l | Resize split in direction |
| Mod+Shift+Q | Close focused window |
| Mod+E | Equalize all splits |
| Mod+Return | Spawn terminal |
Acceptance Criteria
- New windows split intelligently based on container shape
- All keyboard navigation works as specified
- Window swapping maintains tree structure
- Resize adjusts the correct split
- Equalize makes all windows same size
- Closing windows works cleanly
Testing Strategy
# Test smart splits
# Open terminal in empty workspace → full screen
# Open second terminal → should split vertically (side by side)
# Focus right window, open third → right window splits horizontally (stacked)
# Test navigation
# Mod+h/l should move focus left/right
# Mod+j/k should move focus up/down
# Test resize
# Mod+Ctrl+l should make focused window wider
# Mod+E should reset to equal sizes
Notes
- This sprint establishes the core UX that differentiates gar
- Navigation should feel snappy (no perceptible delay)
- Consider adding Mod+Arrow keys as alternative to hjkl
- Direction finding algorithm is the trickiest part - test thoroughly
View source
| 1 | # Sprint 2: Smart Split + Keyboard Navigation |
| 2 | |
| 3 | **Goal:** Implement the core differentiating feature (intelligent split direction) and full keyboard-based window navigation. |
| 4 | |
| 5 | ## Objectives |
| 6 | |
| 7 | - Automatic split direction based on window dimensions |
| 8 | - Full keyboard navigation (vim-style hjkl) |
| 9 | - Keyboard-based window operations |
| 10 | - This is what makes gar different from i3 |
| 11 | |
| 12 | ## Prerequisites |
| 13 | |
| 14 | - Sprint 1 complete (BSP tree, basic window management) |
| 15 | |
| 16 | ## The Smart Split Algorithm |
| 17 | |
| 18 | This is the key feature that Hyprland has and i3 lacks: |
| 19 | |
| 20 | ``` |
| 21 | When inserting a new window: |
| 22 | 1. Get dimensions of the target area (width, height) |
| 23 | 2. If width > height → split Vertical (creates side-by-side windows) |
| 24 | 3. If height >= width → split Horizontal (creates stacked windows) |
| 25 | ``` |
| 26 | |
| 27 | **Visual example:** |
| 28 | ``` |
| 29 | ┌─────────────────────────┐ ┌────────────┬────────────┐ |
| 30 | │ │ │ │ │ |
| 31 | │ Wide container │ --> │ Left │ Right │ |
| 32 | │ (width > height) │ │ │ (new) │ |
| 33 | │ │ │ │ │ |
| 34 | └─────────────────────────┘ └────────────┴────────────┘ |
| 35 | Split VERTICAL |
| 36 | |
| 37 | ┌───────────┐ ┌───────────┐ |
| 38 | │ │ │ Top │ |
| 39 | │ Tall │ ├───────────┤ |
| 40 | │ container │ ────────> │ Bottom │ |
| 41 | │ (h >= w) │ │ (new) │ |
| 42 | │ │ │ │ |
| 43 | └───────────┘ └───────────┘ |
| 44 | Split HORIZONTAL |
| 45 | ``` |
| 46 | |
| 47 | ## Tasks |
| 48 | |
| 49 | ### 2.1 Implement Smart Split |
| 50 | - [ ] Modify `insert_window` in tree.rs |
| 51 | - [ ] Calculate container dimensions before inserting |
| 52 | - [ ] Choose split direction based on width vs height |
| 53 | - [ ] Add unit tests for split direction logic |
| 54 | |
| 55 | ```rust |
| 56 | impl Node { |
| 57 | fn determine_split_direction(rect: &Rect) -> SplitDirection { |
| 58 | if rect.width > rect.height { |
| 59 | SplitDirection::Vertical |
| 60 | } else { |
| 61 | SplitDirection::Horizontal |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | pub fn insert_window(&mut self, window: WindowId, rect: Rect) { |
| 66 | let direction = Self::determine_split_direction(&rect); |
| 67 | // ... rest of insertion logic |
| 68 | } |
| 69 | } |
| 70 | ``` |
| 71 | |
| 72 | ### 2.2 Keyboard Input Infrastructure |
| 73 | - [ ] Create `src/input/mod.rs` and `src/input/keybinds.rs` |
| 74 | - [ ] Define keybind action enum |
| 75 | - [ ] Create keybind registry |
| 76 | - [ ] Grab all configured keys on startup |
| 77 | - [ ] Dispatch to actions on KeyPress event |
| 78 | |
| 79 | ```rust |
| 80 | pub enum Action { |
| 81 | FocusDirection(Direction), |
| 82 | SwapDirection(Direction), |
| 83 | ResizeDirection(Direction, i32), |
| 84 | CloseWindow, |
| 85 | Equalize, |
| 86 | Exec(String), |
| 87 | } |
| 88 | |
| 89 | pub enum Direction { |
| 90 | Left, Right, Up, Down, |
| 91 | } |
| 92 | ``` |
| 93 | |
| 94 | ### 2.3 Focus Navigation |
| 95 | - [ ] Implement directional focus (Mod+h/j/k/l) |
| 96 | - [ ] Find window in given direction from focused |
| 97 | - [ ] Handle edge cases (no window in direction) |
| 98 | - [ ] Update focus and visual feedback |
| 99 | |
| 100 | **Algorithm for finding window in direction:** |
| 101 | ```rust |
| 102 | fn find_window_in_direction( |
| 103 | tree: &Node, |
| 104 | from: WindowId, |
| 105 | direction: Direction, |
| 106 | ) -> Option<WindowId> { |
| 107 | // 1. Get geometry of 'from' window |
| 108 | // 2. Get geometries of all windows |
| 109 | // 3. Filter to windows in the given direction |
| 110 | // 4. Return closest window in that direction |
| 111 | } |
| 112 | ``` |
| 113 | |
| 114 | ### 2.4 Window Closing |
| 115 | - [ ] Implement Mod+Shift+Q to close focused window |
| 116 | - [ ] Send WM_DELETE_WINDOW if supported (ICCCM) |
| 117 | - [ ] Fall back to XKillClient if not |
| 118 | - [ ] Remove from tree after close |
| 119 | |
| 120 | ```rust |
| 121 | fn close_window(conn: &impl Connection, window: Window) -> Result<()> { |
| 122 | // Check if window supports WM_DELETE_WINDOW |
| 123 | if supports_delete_window(conn, window)? { |
| 124 | send_delete_window_message(conn, window)?; |
| 125 | } else { |
| 126 | conn.kill_client(window)?; |
| 127 | } |
| 128 | Ok(()) |
| 129 | } |
| 130 | ``` |
| 131 | |
| 132 | ### 2.5 Resize Splits |
| 133 | - [ ] Implement Mod+Ctrl+h/j/k/l to resize |
| 134 | - [ ] Find the split that affects the focused window in that direction |
| 135 | - [ ] Adjust ratio by fixed increment (e.g., 0.05) |
| 136 | - [ ] Clamp ratio between 0.1 and 0.9 |
| 137 | - [ ] Recalculate and apply geometries |
| 138 | |
| 139 | ```rust |
| 140 | fn resize_in_direction(tree: &mut Node, window: WindowId, dir: Direction, delta: f32) { |
| 141 | // Find the nearest ancestor split in the given direction |
| 142 | // Adjust its ratio |
| 143 | } |
| 144 | ``` |
| 145 | |
| 146 | ### 2.6 Equalize Splits |
| 147 | - [ ] Implement Mod+E to equalize |
| 148 | - [ ] Set all split ratios to 0.5 |
| 149 | - [ ] Recursive traversal of tree |
| 150 | - [ ] Recalculate and apply geometries |
| 151 | |
| 152 | ```rust |
| 153 | fn equalize(node: &mut Node) { |
| 154 | match node { |
| 155 | Node::Internal { ratio, left, right, .. } => { |
| 156 | *ratio = 0.5; |
| 157 | equalize(left); |
| 158 | equalize(right); |
| 159 | } |
| 160 | Node::Leaf { .. } => {} |
| 161 | } |
| 162 | } |
| 163 | ``` |
| 164 | |
| 165 | ### 2.7 Swap Windows |
| 166 | - [ ] Implement Mod+Shift+h/j/k/l to swap |
| 167 | - [ ] Find window in direction |
| 168 | - [ ] Swap the two windows in the tree |
| 169 | - [ ] Recalculate and apply geometries |
| 170 | |
| 171 | ```rust |
| 172 | fn swap_windows(tree: &mut Node, a: WindowId, b: WindowId) { |
| 173 | // Find both leaf nodes |
| 174 | // Swap their window IDs |
| 175 | } |
| 176 | ``` |
| 177 | |
| 178 | ## Keybind Summary |
| 179 | |
| 180 | | Keybind | Action | |
| 181 | |---------|--------| |
| 182 | | Mod+h/j/k/l | Focus left/down/up/right | |
| 183 | | Mod+Shift+h/j/k/l | Swap with left/down/up/right | |
| 184 | | Mod+Ctrl+h/j/k/l | Resize split in direction | |
| 185 | | Mod+Shift+Q | Close focused window | |
| 186 | | Mod+E | Equalize all splits | |
| 187 | | Mod+Return | Spawn terminal | |
| 188 | |
| 189 | ## Acceptance Criteria |
| 190 | |
| 191 | 1. New windows split intelligently based on container shape |
| 192 | 2. All keyboard navigation works as specified |
| 193 | 3. Window swapping maintains tree structure |
| 194 | 4. Resize adjusts the correct split |
| 195 | 5. Equalize makes all windows same size |
| 196 | 6. Closing windows works cleanly |
| 197 | |
| 198 | ## Testing Strategy |
| 199 | |
| 200 | ```bash |
| 201 | # Test smart splits |
| 202 | # Open terminal in empty workspace → full screen |
| 203 | # Open second terminal → should split vertically (side by side) |
| 204 | # Focus right window, open third → right window splits horizontally (stacked) |
| 205 | |
| 206 | # Test navigation |
| 207 | # Mod+h/l should move focus left/right |
| 208 | # Mod+j/k should move focus up/down |
| 209 | |
| 210 | # Test resize |
| 211 | # Mod+Ctrl+l should make focused window wider |
| 212 | # Mod+E should reset to equal sizes |
| 213 | ``` |
| 214 | |
| 215 | ## Notes |
| 216 | |
| 217 | - This sprint establishes the core UX that differentiates gar |
| 218 | - Navigation should feel snappy (no perceptible delay) |
| 219 | - Consider adding Mod+Arrow keys as alternative to hjkl |
| 220 | - Direction finding algorithm is the trickiest part - test thoroughly |