markdown · 6296 bytes Raw Blame History

Sprint 3: Multiple Workspaces

Goal: Implement named workspaces with switching and window movement.

Objectives

  • Multiple independent workspaces, each with its own BSP tree
  • Keyboard-based workspace switching
  • Move windows between workspaces
  • EWMH workspace properties for status bar integration

Prerequisites

  • Sprint 2 complete (smart split, keyboard navigation)

Tasks

3.1 Workspace Data Structure

  • Create src/core/workspace.rs
  • Define Workspace struct
  • Create WorkspaceManager to hold all workspaces
  • Initialize default workspaces (1-10)
pub struct Workspace {
    pub id: WorkspaceId,
    pub name: String,
    pub tree: Node,
    pub floating: Vec<Window>,
    pub focused: Option<WindowId>,
    pub visible: bool,
}

pub struct WorkspaceManager {
    workspaces: Vec<Workspace>,
    active: WorkspaceId,
}

3.2 Workspace Initialization

  • Create 10 default workspaces (named "1" through "10")
  • Set workspace 1 as active on startup
  • Existing windows go to workspace 1
impl WorkspaceManager {
    pub fn new() -> Self {
        let workspaces = (1..=10)
            .map(|i| Workspace {
                id: WorkspaceId(i),
                name: i.to_string(),
                tree: Node::empty(),
                floating: Vec::new(),
                focused: None,
                visible: i == 1,
            })
            .collect();
        Self { workspaces, active: WorkspaceId(1) }
    }
}

3.3 Workspace Switching

  • Implement Mod+1 through Mod+9 (and Mod+0 for 10)
  • Hide windows on old workspace
  • Show windows on new workspace
  • Restore focus to previously focused window
  • Update EWMH _NET_CURRENT_DESKTOP
fn switch_workspace(&mut self, conn: &impl Connection, target: WorkspaceId) -> Result<()> {
    if target == self.active {
        return Ok(());
    }

    // Hide current workspace windows
    for window in self.current_workspace().all_windows() {
        conn.unmap_window(window)?;
    }

    // Show target workspace windows
    self.active = target;
    for window in self.current_workspace().all_windows() {
        conn.map_window(window)?;
    }

    // Restore focus
    if let Some(focused) = self.current_workspace().focused {
        set_focus(conn, focused)?;
    }

    Ok(())
}

3.4 Move Window to Workspace

  • Implement Mod+Shift+1 through Mod+Shift+9
  • Remove window from current workspace tree
  • Add window to target workspace tree
  • If target workspace is not visible, unmap window
  • Optionally follow window to new workspace (configurable later)
fn move_to_workspace(
    &mut self,
    conn: &impl Connection,
    window: WindowId,
    target: WorkspaceId,
) -> Result<()> {
    // Remove from current
    self.current_workspace_mut().tree.remove(window);

    // Add to target
    self.workspace_mut(target).tree.insert(window);

    // Hide if target not visible
    if target != self.active {
        conn.unmap_window(window)?;
    }

    // Retile both workspaces
    self.apply_layout(conn, self.active)?;
    self.apply_layout(conn, target)?;

    Ok(())
}

3.5 Per-Workspace Trees

  • Modify window insertion to use active workspace
  • Modify window removal to find correct workspace
  • Ensure tree operations target correct workspace
  • Handle window destroy when on non-active workspace

3.6 EWMH Workspace Support

  • Set _NET_NUMBER_OF_DESKTOPS on root
  • Set _NET_DESKTOP_NAMES with workspace names
  • Set _NET_CURRENT_DESKTOP when switching
  • Set _NET_WM_DESKTOP on each window
  • Handle _NET_CURRENT_DESKTOP client messages (pagers)
fn set_ewmh_workspace_hints(conn: &impl Connection, manager: &WorkspaceManager) -> Result<()> {
    let root = get_root(conn);

    // Number of desktops
    conn.change_property32(
        PropMode::REPLACE,
        root,
        atoms._NET_NUMBER_OF_DESKTOPS,
        AtomEnum::CARDINAL,
        &[manager.workspaces.len() as u32],
    )?;

    // Desktop names
    let names: Vec<u8> = manager.workspaces
        .iter()
        .flat_map(|ws| ws.name.bytes().chain(std::iter::once(0)))
        .collect();
    conn.change_property8(
        PropMode::REPLACE,
        root,
        atoms._NET_DESKTOP_NAMES,
        atoms.UTF8_STRING,
        &names,
    )?;

    // Current desktop
    conn.change_property32(
        PropMode::REPLACE,
        root,
        atoms._NET_CURRENT_DESKTOP,
        AtomEnum::CARDINAL,
        &[manager.active.0 as u32 - 1], // 0-indexed for EWMH
    )?;

    Ok(())
}

3.7 Handle Existing Windows on Startup

  • Query existing windows on WM start
  • Check _NET_WM_DESKTOP for previous workspace assignment
  • Place windows in appropriate workspaces
  • Map windows in active workspace only

Keybind Summary

Keybind Action
Mod+1-9 Switch to workspace 1-9
Mod+0 Switch to workspace 10
Mod+Shift+1-9 Move window to workspace 1-9
Mod+Shift+0 Move window to workspace 10

Acceptance Criteria

  1. Can switch between 10 workspaces
  2. Each workspace maintains its own window layout
  3. Moving window to another workspace works
  4. Status bars (polybar) can display workspace info
  5. Windows hidden/shown correctly on switch
  6. Focus restored correctly when switching back

Testing Strategy

# Test workspace switching
# Mod+1 - should be on workspace 1
# Open xterm
# Mod+2 - switch to workspace 2 (empty)
# Open xterm - should be alone
# Mod+1 - back to workspace 1, original xterm visible

# Test window movement
# On workspace 1 with xterm
# Mod+Shift+2 - moves xterm to workspace 2
# Mod+2 - verify xterm is there

# Test polybar integration
polybar example  # Should show workspace indicators

Status Bar Integration

For polybar, create an i3-compatible module (we'll implement IPC in Sprint 6):

[module/workspaces]
type = internal/xworkspaces
label-active = %name%
label-occupied = %name%
label-empty = %name%

Notes

  • 0-indexed for EWMH, 1-indexed for user display
  • Consider urgent workspace highlighting (Sprint 8)
  • Named workspaces beyond 1-10 come with Lua config (Sprint 4)
  • Multi-monitor workspace assignment comes in Sprint 7
View source
1 # Sprint 3: Multiple Workspaces
2
3 **Goal:** Implement named workspaces with switching and window movement.
4
5 ## Objectives
6
7 - Multiple independent workspaces, each with its own BSP tree
8 - Keyboard-based workspace switching
9 - Move windows between workspaces
10 - EWMH workspace properties for status bar integration
11
12 ## Prerequisites
13
14 - Sprint 2 complete (smart split, keyboard navigation)
15
16 ## Tasks
17
18 ### 3.1 Workspace Data Structure
19 - [ ] Create `src/core/workspace.rs`
20 - [ ] Define `Workspace` struct
21 - [ ] Create `WorkspaceManager` to hold all workspaces
22 - [ ] Initialize default workspaces (1-10)
23
24 ```rust
25 pub struct Workspace {
26 pub id: WorkspaceId,
27 pub name: String,
28 pub tree: Node,
29 pub floating: Vec<Window>,
30 pub focused: Option<WindowId>,
31 pub visible: bool,
32 }
33
34 pub struct WorkspaceManager {
35 workspaces: Vec<Workspace>,
36 active: WorkspaceId,
37 }
38 ```
39
40 ### 3.2 Workspace Initialization
41 - [ ] Create 10 default workspaces (named "1" through "10")
42 - [ ] Set workspace 1 as active on startup
43 - [ ] Existing windows go to workspace 1
44
45 ```rust
46 impl WorkspaceManager {
47 pub fn new() -> Self {
48 let workspaces = (1..=10)
49 .map(|i| Workspace {
50 id: WorkspaceId(i),
51 name: i.to_string(),
52 tree: Node::empty(),
53 floating: Vec::new(),
54 focused: None,
55 visible: i == 1,
56 })
57 .collect();
58 Self { workspaces, active: WorkspaceId(1) }
59 }
60 }
61 ```
62
63 ### 3.3 Workspace Switching
64 - [ ] Implement Mod+1 through Mod+9 (and Mod+0 for 10)
65 - [ ] Hide windows on old workspace
66 - [ ] Show windows on new workspace
67 - [ ] Restore focus to previously focused window
68 - [ ] Update EWMH _NET_CURRENT_DESKTOP
69
70 ```rust
71 fn switch_workspace(&mut self, conn: &impl Connection, target: WorkspaceId) -> Result<()> {
72 if target == self.active {
73 return Ok(());
74 }
75
76 // Hide current workspace windows
77 for window in self.current_workspace().all_windows() {
78 conn.unmap_window(window)?;
79 }
80
81 // Show target workspace windows
82 self.active = target;
83 for window in self.current_workspace().all_windows() {
84 conn.map_window(window)?;
85 }
86
87 // Restore focus
88 if let Some(focused) = self.current_workspace().focused {
89 set_focus(conn, focused)?;
90 }
91
92 Ok(())
93 }
94 ```
95
96 ### 3.4 Move Window to Workspace
97 - [ ] Implement Mod+Shift+1 through Mod+Shift+9
98 - [ ] Remove window from current workspace tree
99 - [ ] Add window to target workspace tree
100 - [ ] If target workspace is not visible, unmap window
101 - [ ] Optionally follow window to new workspace (configurable later)
102
103 ```rust
104 fn move_to_workspace(
105 &mut self,
106 conn: &impl Connection,
107 window: WindowId,
108 target: WorkspaceId,
109 ) -> Result<()> {
110 // Remove from current
111 self.current_workspace_mut().tree.remove(window);
112
113 // Add to target
114 self.workspace_mut(target).tree.insert(window);
115
116 // Hide if target not visible
117 if target != self.active {
118 conn.unmap_window(window)?;
119 }
120
121 // Retile both workspaces
122 self.apply_layout(conn, self.active)?;
123 self.apply_layout(conn, target)?;
124
125 Ok(())
126 }
127 ```
128
129 ### 3.5 Per-Workspace Trees
130 - [ ] Modify window insertion to use active workspace
131 - [ ] Modify window removal to find correct workspace
132 - [ ] Ensure tree operations target correct workspace
133 - [ ] Handle window destroy when on non-active workspace
134
135 ### 3.6 EWMH Workspace Support
136 - [ ] Set `_NET_NUMBER_OF_DESKTOPS` on root
137 - [ ] Set `_NET_DESKTOP_NAMES` with workspace names
138 - [ ] Set `_NET_CURRENT_DESKTOP` when switching
139 - [ ] Set `_NET_WM_DESKTOP` on each window
140 - [ ] Handle `_NET_CURRENT_DESKTOP` client messages (pagers)
141
142 ```rust
143 fn set_ewmh_workspace_hints(conn: &impl Connection, manager: &WorkspaceManager) -> Result<()> {
144 let root = get_root(conn);
145
146 // Number of desktops
147 conn.change_property32(
148 PropMode::REPLACE,
149 root,
150 atoms._NET_NUMBER_OF_DESKTOPS,
151 AtomEnum::CARDINAL,
152 &[manager.workspaces.len() as u32],
153 )?;
154
155 // Desktop names
156 let names: Vec<u8> = manager.workspaces
157 .iter()
158 .flat_map(|ws| ws.name.bytes().chain(std::iter::once(0)))
159 .collect();
160 conn.change_property8(
161 PropMode::REPLACE,
162 root,
163 atoms._NET_DESKTOP_NAMES,
164 atoms.UTF8_STRING,
165 &names,
166 )?;
167
168 // Current desktop
169 conn.change_property32(
170 PropMode::REPLACE,
171 root,
172 atoms._NET_CURRENT_DESKTOP,
173 AtomEnum::CARDINAL,
174 &[manager.active.0 as u32 - 1], // 0-indexed for EWMH
175 )?;
176
177 Ok(())
178 }
179 ```
180
181 ### 3.7 Handle Existing Windows on Startup
182 - [ ] Query existing windows on WM start
183 - [ ] Check `_NET_WM_DESKTOP` for previous workspace assignment
184 - [ ] Place windows in appropriate workspaces
185 - [ ] Map windows in active workspace only
186
187 ## Keybind Summary
188
189 | Keybind | Action |
190 |---------|--------|
191 | Mod+1-9 | Switch to workspace 1-9 |
192 | Mod+0 | Switch to workspace 10 |
193 | Mod+Shift+1-9 | Move window to workspace 1-9 |
194 | Mod+Shift+0 | Move window to workspace 10 |
195
196 ## Acceptance Criteria
197
198 1. Can switch between 10 workspaces
199 2. Each workspace maintains its own window layout
200 3. Moving window to another workspace works
201 4. Status bars (polybar) can display workspace info
202 5. Windows hidden/shown correctly on switch
203 6. Focus restored correctly when switching back
204
205 ## Testing Strategy
206
207 ```bash
208 # Test workspace switching
209 # Mod+1 - should be on workspace 1
210 # Open xterm
211 # Mod+2 - switch to workspace 2 (empty)
212 # Open xterm - should be alone
213 # Mod+1 - back to workspace 1, original xterm visible
214
215 # Test window movement
216 # On workspace 1 with xterm
217 # Mod+Shift+2 - moves xterm to workspace 2
218 # Mod+2 - verify xterm is there
219
220 # Test polybar integration
221 polybar example # Should show workspace indicators
222 ```
223
224 ## Status Bar Integration
225
226 For polybar, create an i3-compatible module (we'll implement IPC in Sprint 6):
227
228 ```ini
229 [module/workspaces]
230 type = internal/xworkspaces
231 label-active = %name%
232 label-occupied = %name%
233 label-empty = %name%
234 ```
235
236 ## Notes
237
238 - 0-indexed for EWMH, 1-indexed for user display
239 - Consider urgent workspace highlighting (Sprint 8)
240 - Named workspaces beyond 1-10 come with Lua config (Sprint 4)
241 - Multi-monitor workspace assignment comes in Sprint 7