markdown · 6622 bytes Raw Blame History

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_window in 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.rs and src/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

  1. New windows split intelligently based on container shape
  2. All keyboard navigation works as specified
  3. Window swapping maintains tree structure
  4. Resize adjusts the correct split
  5. Equalize makes all windows same size
  6. 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