markdown · 8364 bytes Raw Blame History

Sprint 6: IPC System

Goal: External control via Unix socket with JSON protocol.

Objectives

  • Unix socket server for IPC
  • JSON-based command/response protocol
  • Command-line tool (garctl) for interaction
  • Event subscription for external tools

Prerequisites

  • Sprint 5 complete (floating windows)

Protocol Design

Message Format

// Command (client -> server)
{
    "type": "command",
    "command": "focus",
    "args": { "direction": "left" }
}

// Response (server -> client)
{
    "type": "response",
    "success": true,
    "data": { /* optional result */ }
}

// Error response
{
    "type": "response",
    "success": false,
    "error": "Window not found"
}

// Event (server -> subscribed clients)
{
    "type": "event",
    "event": "window_focus",
    "data": { "window_id": 12345, "title": "Terminal" }
}

Commands

Command Args Description
focus direction Focus window in direction
swap direction Swap with window in direction
resize direction, amount Resize split
close - Close focused window
workspace number Switch to workspace
move_to_workspace number Move window to workspace
toggle_floating - Toggle floating state
reload - Reload configuration
exit - Exit gar
get_tree - Get window tree
get_workspaces - Get workspace info
get_focused - Get focused window
subscribe events[] Subscribe to events

Events

Event Data Description
window_new window info New window created
window_close window_id Window closed
window_focus window info Focus changed
window_move window_id, workspace Window moved
workspace_focus workspace info Workspace changed
mode mode name Mode changed (later)

Tasks

6.1 Socket Server Setup

  • Create src/ipc/mod.rs, server.rs, protocol.rs
  • Add tokio dependency for async I/O
  • Create Unix socket at $XDG_RUNTIME_DIR/gar.sock
  • Handle multiple concurrent clients
  • Clean up socket on exit
use tokio::net::{UnixListener, UnixStream};

pub struct IpcServer {
    listener: UnixListener,
    clients: Vec<Client>,
}

impl IpcServer {
    pub async fn new() -> Result<Self> {
        let path = std::env::var("XDG_RUNTIME_DIR")
            .map(|dir| format!("{}/gar.sock", dir))
            .unwrap_or_else(|_| "/tmp/gar.sock".to_string());

        // Remove existing socket
        let _ = std::fs::remove_file(&path);

        let listener = UnixListener::bind(&path)?;
        Ok(Self { listener, clients: Vec::new() })
    }
}

6.2 Protocol Types

  • Define message types with serde
  • Implement JSON serialization
  • Handle malformed messages gracefully
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
pub enum Request {
    #[serde(rename = "command")]
    Command { command: String, args: serde_json::Value },
}

#[derive(Debug, Serialize)]
#[serde(tag = "type")]
pub enum Response {
    #[serde(rename = "response")]
    Success { success: bool, data: Option<serde_json::Value> },
    #[serde(rename = "response")]
    Error { success: bool, error: String },
}

#[derive(Debug, Serialize)]
#[serde(tag = "type")]
pub enum Event {
    #[serde(rename = "event")]
    Event { event: String, data: serde_json::Value },
}

6.3 Command Dispatch

  • Parse incoming commands
  • Map to window manager actions
  • Execute and return result
  • Handle unknown commands
impl IpcServer {
    fn dispatch(&self, wm: &mut WindowManager, cmd: &str, args: Value) -> Response {
        match cmd {
            "focus" => {
                let direction = args["direction"].as_str().unwrap();
                wm.focus_direction(direction.parse()?)?;
                Response::success(None)
            }
            "get_tree" => {
                let tree = wm.get_tree_json();
                Response::success(Some(tree))
            }
            _ => Response::error(format!("Unknown command: {}", cmd)),
        }
    }
}

6.4 Query Commands

  • get_tree - return workspace trees as JSON
  • get_workspaces - return workspace list
  • get_focused - return focused window info
  • get_outputs - return monitor info (for multi-monitor)
fn get_tree_json(&self) -> Value {
    json!({
        "workspaces": self.workspaces.iter().map(|ws| {
            json!({
                "name": ws.name,
                "focused": ws.focused,
                "nodes": tree_to_json(&ws.tree),
            })
        }).collect::<Vec<_>>()
    })
}

6.5 Event Subscription

  • Track subscribed clients per event type
  • Broadcast events to subscribers
  • Handle client disconnection
  • Implement subscription command
struct Client {
    stream: UnixStream,
    subscriptions: HashSet<String>,
}

impl IpcServer {
    fn broadcast_event(&mut self, event: &str, data: Value) {
        let msg = Event { event: event.into(), data };
        let json = serde_json::to_string(&msg).unwrap();

        self.clients.retain(|client| {
            if client.subscriptions.contains(event) {
                client.stream.try_write(json.as_bytes()).is_ok()
            } else {
                true
            }
        });
    }
}

6.6 Integration with Event Loop

  • Run IPC server alongside X event loop
  • Use tokio runtime or poll-based approach
  • Handle commands without blocking X events
  • Thread-safe communication with WM state
// Option 1: Tokio with channels
fn main() {
    let (cmd_tx, cmd_rx) = tokio::sync::mpsc::channel(100);
    let (event_tx, event_rx) = tokio::sync::broadcast::channel(100);

    // Spawn IPC server task
    tokio::spawn(async move {
        ipc_server.run(cmd_tx, event_rx).await;
    });

    // X event loop
    loop {
        // Check for IPC commands
        while let Ok(cmd) = cmd_rx.try_recv() {
            handle_ipc_command(cmd);
        }

        // Handle X events
        let event = conn.wait_for_event()?;
        handle_x_event(event);

        // Broadcast events
        event_tx.send(/* ... */);
    }
}

6.7 garctl CLI Tool

  • Create garctl/src/main.rs
  • Connect to gar socket
  • Send commands from CLI args
  • Print responses
  • Support event monitoring mode
// garctl/src/main.rs
use clap::Parser;

#[derive(Parser)]
struct Cli {
    #[clap(subcommand)]
    command: Command,
}

#[derive(Parser)]
enum Command {
    Focus { direction: String },
    Workspace { number: u32 },
    GetTree,
    Subscribe { events: Vec<String> },
    // ...
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    let mut socket = UnixStream::connect(get_socket_path())?;

    let request = match cli.command {
        Command::Focus { direction } => {
            json!({ "type": "command", "command": "focus", "args": { "direction": direction } })
        }
        // ...
    };

    socket.write_all(serde_json::to_string(&request)?.as_bytes())?;

    let mut response = String::new();
    socket.read_to_string(&mut response)?;
    println!("{}", response);

    Ok(())
}

garctl Usage

# Focus direction
garctl focus left
garctl focus right

# Workspaces
garctl workspace 2
garctl move-to-workspace 3

# Queries
garctl get-tree
garctl get-workspaces
garctl get-focused

# Events (stays open, prints events)
garctl subscribe window_focus workspace_focus

Acceptance Criteria

  1. Socket created at $XDG_RUNTIME_DIR/gar.sock
  2. garctl can send commands and receive responses
  3. All WM actions available via IPC
  4. Query commands return proper JSON
  5. Event subscription works for focus/workspace changes
  6. Multiple concurrent clients supported

Testing Strategy

# Start gar
DISPLAY=:1 cargo run

# Test command
garctl focus left  # Should return success

# Test query
garctl get-tree | jq .  # Should show tree structure

# Test events
garctl subscribe window_focus &
# Open/focus windows, events should print

Notes

  • Socket permissions should be user-only (0600)
  • Consider i3-compatible message format for polybar compatibility
  • Add timeout for client reads to prevent blocking
  • Log all IPC activity for debugging
View source
1 # Sprint 6: IPC System
2
3 **Goal:** External control via Unix socket with JSON protocol.
4
5 ## Objectives
6
7 - Unix socket server for IPC
8 - JSON-based command/response protocol
9 - Command-line tool (`garctl`) for interaction
10 - Event subscription for external tools
11
12 ## Prerequisites
13
14 - Sprint 5 complete (floating windows)
15
16 ## Protocol Design
17
18 ### Message Format
19
20 ```json
21 // Command (client -> server)
22 {
23 "type": "command",
24 "command": "focus",
25 "args": { "direction": "left" }
26 }
27
28 // Response (server -> client)
29 {
30 "type": "response",
31 "success": true,
32 "data": { /* optional result */ }
33 }
34
35 // Error response
36 {
37 "type": "response",
38 "success": false,
39 "error": "Window not found"
40 }
41
42 // Event (server -> subscribed clients)
43 {
44 "type": "event",
45 "event": "window_focus",
46 "data": { "window_id": 12345, "title": "Terminal" }
47 }
48 ```
49
50 ### Commands
51
52 | Command | Args | Description |
53 |---------|------|-------------|
54 | focus | direction | Focus window in direction |
55 | swap | direction | Swap with window in direction |
56 | resize | direction, amount | Resize split |
57 | close | - | Close focused window |
58 | workspace | number | Switch to workspace |
59 | move_to_workspace | number | Move window to workspace |
60 | toggle_floating | - | Toggle floating state |
61 | reload | - | Reload configuration |
62 | exit | - | Exit gar |
63 | get_tree | - | Get window tree |
64 | get_workspaces | - | Get workspace info |
65 | get_focused | - | Get focused window |
66 | subscribe | events[] | Subscribe to events |
67
68 ### Events
69
70 | Event | Data | Description |
71 |-------|------|-------------|
72 | window_new | window info | New window created |
73 | window_close | window_id | Window closed |
74 | window_focus | window info | Focus changed |
75 | window_move | window_id, workspace | Window moved |
76 | workspace_focus | workspace info | Workspace changed |
77 | mode | mode name | Mode changed (later) |
78
79 ## Tasks
80
81 ### 6.1 Socket Server Setup
82 - [ ] Create `src/ipc/mod.rs`, `server.rs`, `protocol.rs`
83 - [ ] Add tokio dependency for async I/O
84 - [ ] Create Unix socket at `$XDG_RUNTIME_DIR/gar.sock`
85 - [ ] Handle multiple concurrent clients
86 - [ ] Clean up socket on exit
87
88 ```rust
89 use tokio::net::{UnixListener, UnixStream};
90
91 pub struct IpcServer {
92 listener: UnixListener,
93 clients: Vec<Client>,
94 }
95
96 impl IpcServer {
97 pub async fn new() -> Result<Self> {
98 let path = std::env::var("XDG_RUNTIME_DIR")
99 .map(|dir| format!("{}/gar.sock", dir))
100 .unwrap_or_else(|_| "/tmp/gar.sock".to_string());
101
102 // Remove existing socket
103 let _ = std::fs::remove_file(&path);
104
105 let listener = UnixListener::bind(&path)?;
106 Ok(Self { listener, clients: Vec::new() })
107 }
108 }
109 ```
110
111 ### 6.2 Protocol Types
112 - [ ] Define message types with serde
113 - [ ] Implement JSON serialization
114 - [ ] Handle malformed messages gracefully
115
116 ```rust
117 use serde::{Deserialize, Serialize};
118
119 #[derive(Debug, Deserialize)]
120 #[serde(tag = "type")]
121 pub enum Request {
122 #[serde(rename = "command")]
123 Command { command: String, args: serde_json::Value },
124 }
125
126 #[derive(Debug, Serialize)]
127 #[serde(tag = "type")]
128 pub enum Response {
129 #[serde(rename = "response")]
130 Success { success: bool, data: Option<serde_json::Value> },
131 #[serde(rename = "response")]
132 Error { success: bool, error: String },
133 }
134
135 #[derive(Debug, Serialize)]
136 #[serde(tag = "type")]
137 pub enum Event {
138 #[serde(rename = "event")]
139 Event { event: String, data: serde_json::Value },
140 }
141 ```
142
143 ### 6.3 Command Dispatch
144 - [ ] Parse incoming commands
145 - [ ] Map to window manager actions
146 - [ ] Execute and return result
147 - [ ] Handle unknown commands
148
149 ```rust
150 impl IpcServer {
151 fn dispatch(&self, wm: &mut WindowManager, cmd: &str, args: Value) -> Response {
152 match cmd {
153 "focus" => {
154 let direction = args["direction"].as_str().unwrap();
155 wm.focus_direction(direction.parse()?)?;
156 Response::success(None)
157 }
158 "get_tree" => {
159 let tree = wm.get_tree_json();
160 Response::success(Some(tree))
161 }
162 _ => Response::error(format!("Unknown command: {}", cmd)),
163 }
164 }
165 }
166 ```
167
168 ### 6.4 Query Commands
169 - [ ] `get_tree` - return workspace trees as JSON
170 - [ ] `get_workspaces` - return workspace list
171 - [ ] `get_focused` - return focused window info
172 - [ ] `get_outputs` - return monitor info (for multi-monitor)
173
174 ```rust
175 fn get_tree_json(&self) -> Value {
176 json!({
177 "workspaces": self.workspaces.iter().map(|ws| {
178 json!({
179 "name": ws.name,
180 "focused": ws.focused,
181 "nodes": tree_to_json(&ws.tree),
182 })
183 }).collect::<Vec<_>>()
184 })
185 }
186 ```
187
188 ### 6.5 Event Subscription
189 - [ ] Track subscribed clients per event type
190 - [ ] Broadcast events to subscribers
191 - [ ] Handle client disconnection
192 - [ ] Implement subscription command
193
194 ```rust
195 struct Client {
196 stream: UnixStream,
197 subscriptions: HashSet<String>,
198 }
199
200 impl IpcServer {
201 fn broadcast_event(&mut self, event: &str, data: Value) {
202 let msg = Event { event: event.into(), data };
203 let json = serde_json::to_string(&msg).unwrap();
204
205 self.clients.retain(|client| {
206 if client.subscriptions.contains(event) {
207 client.stream.try_write(json.as_bytes()).is_ok()
208 } else {
209 true
210 }
211 });
212 }
213 }
214 ```
215
216 ### 6.6 Integration with Event Loop
217 - [ ] Run IPC server alongside X event loop
218 - [ ] Use tokio runtime or poll-based approach
219 - [ ] Handle commands without blocking X events
220 - [ ] Thread-safe communication with WM state
221
222 ```rust
223 // Option 1: Tokio with channels
224 fn main() {
225 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::channel(100);
226 let (event_tx, event_rx) = tokio::sync::broadcast::channel(100);
227
228 // Spawn IPC server task
229 tokio::spawn(async move {
230 ipc_server.run(cmd_tx, event_rx).await;
231 });
232
233 // X event loop
234 loop {
235 // Check for IPC commands
236 while let Ok(cmd) = cmd_rx.try_recv() {
237 handle_ipc_command(cmd);
238 }
239
240 // Handle X events
241 let event = conn.wait_for_event()?;
242 handle_x_event(event);
243
244 // Broadcast events
245 event_tx.send(/* ... */);
246 }
247 }
248 ```
249
250 ### 6.7 garctl CLI Tool
251 - [ ] Create `garctl/src/main.rs`
252 - [ ] Connect to gar socket
253 - [ ] Send commands from CLI args
254 - [ ] Print responses
255 - [ ] Support event monitoring mode
256
257 ```rust
258 // garctl/src/main.rs
259 use clap::Parser;
260
261 #[derive(Parser)]
262 struct Cli {
263 #[clap(subcommand)]
264 command: Command,
265 }
266
267 #[derive(Parser)]
268 enum Command {
269 Focus { direction: String },
270 Workspace { number: u32 },
271 GetTree,
272 Subscribe { events: Vec<String> },
273 // ...
274 }
275
276 fn main() -> Result<()> {
277 let cli = Cli::parse();
278 let mut socket = UnixStream::connect(get_socket_path())?;
279
280 let request = match cli.command {
281 Command::Focus { direction } => {
282 json!({ "type": "command", "command": "focus", "args": { "direction": direction } })
283 }
284 // ...
285 };
286
287 socket.write_all(serde_json::to_string(&request)?.as_bytes())?;
288
289 let mut response = String::new();
290 socket.read_to_string(&mut response)?;
291 println!("{}", response);
292
293 Ok(())
294 }
295 ```
296
297 ## garctl Usage
298
299 ```bash
300 # Focus direction
301 garctl focus left
302 garctl focus right
303
304 # Workspaces
305 garctl workspace 2
306 garctl move-to-workspace 3
307
308 # Queries
309 garctl get-tree
310 garctl get-workspaces
311 garctl get-focused
312
313 # Events (stays open, prints events)
314 garctl subscribe window_focus workspace_focus
315 ```
316
317 ## Acceptance Criteria
318
319 1. Socket created at `$XDG_RUNTIME_DIR/gar.sock`
320 2. `garctl` can send commands and receive responses
321 3. All WM actions available via IPC
322 4. Query commands return proper JSON
323 5. Event subscription works for focus/workspace changes
324 6. Multiple concurrent clients supported
325
326 ## Testing Strategy
327
328 ```bash
329 # Start gar
330 DISPLAY=:1 cargo run
331
332 # Test command
333 garctl focus left # Should return success
334
335 # Test query
336 garctl get-tree | jq . # Should show tree structure
337
338 # Test events
339 garctl subscribe window_focus &
340 # Open/focus windows, events should print
341 ```
342
343 ## Notes
344
345 - Socket permissions should be user-only (0600)
346 - Consider i3-compatible message format for polybar compatibility
347 - Add timeout for client reads to prevent blocking
348 - Log all IPC activity for debugging