gardesk/gar / 43fd5b4

Browse files

i3 IPC compatibility for polybar integration

- Binary protocol: "i3-ipc" + length + type + JSON payload
- Socket at $XDG_RUNTIME_DIR/gar-i3.sock, sets I3SOCK env var
- Implements GET_WORKSPACES, GET_OUTPUTS, SUBSCRIBE, GET_VERSION
- Broadcasts workspace/output events for real-time updates
- Enables polybar's i3 module for per-monitor workspace indicators
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
43fd5b4f3a81328bf0f9da770f42fef51e315a79
Parents
6bf1f71
Tree
16846a3

5 changed files

StatusFile+-
M gar/src/core/mod.rs 13 1
A gar/src/ipc/i3_compat.rs 196 0
A gar/src/ipc/i3_server.rs 337 0
M gar/src/ipc/mod.rs 3 0
M gar/src/x11/events.rs 194 0
gar/src/core/mod.rsmodified
@@ -13,7 +13,7 @@ use std::sync::{Arc, Mutex};
1313
 use x11rb::protocol::xproto::{ConnectionExt, Window as XWindow};
1414
 
1515
 use crate::config::{Config, LuaConfig, LuaState, RuleActions, WindowMatch};
16
-use crate::ipc::IpcServer;
16
+use crate::ipc::{IpcServer, I3IpcServer};
1717
 use crate::x11::Connection;
1818
 use crate::x11::events::DragState;
1919
 use crate::x11::FrameManager;
@@ -33,6 +33,8 @@ pub struct WindowManager {
3333
     pub running: bool,
3434
     pub drag_state: Option<DragState>,
3535
     pub ipc_server: Option<IpcServer>,
36
+    /// i3-compatible IPC server for polybar integration
37
+    pub i3_ipc_server: Option<I3IpcServer>,
3638
     /// Timestamp of last pointer warp - used to suppress EnterNotify feedback loop
3739
     pub last_warp: std::time::Instant,
3840
     /// Frame manager for title bars
@@ -66,6 +68,15 @@ impl WindowManager {
6668
             }
6769
         };
6870
 
71
+        // Initialize i3-compatible IPC server (optional - graceful failure)
72
+        let i3_ipc_server = match I3IpcServer::new() {
73
+            Ok(server) => Some(server),
74
+            Err(e) => {
75
+                tracing::warn!("Failed to start i3-compatible IPC server: {}", e);
76
+                None
77
+            }
78
+        };
79
+
6980
         // Subscribe to RandR events for hotplug
7081
         if let Err(e) = conn.subscribe_randr_events() {
7182
             tracing::warn!("Failed to subscribe to RandR events: {}", e);
@@ -112,6 +123,7 @@ impl WindowManager {
112123
             running: true,
113124
             drag_state: None,
114125
             ipc_server,
126
+            i3_ipc_server,
115127
             last_warp: std::time::Instant::now(),
116128
             frames: FrameManager::new(),
117129
         })
gar/src/ipc/i3_compat.rsadded
@@ -0,0 +1,196 @@
1
+//! i3 IPC wire protocol implementation.
2
+//!
3
+//! Binary format: "i3-ipc" (6 bytes) + length (u32 LE) + type (u32 LE) + JSON payload
4
+//! Events have the high bit set: 0x80000000 | message_type
5
+
6
+use std::io::{self, Read, Write};
7
+
8
+/// Magic string that prefixes all i3 IPC messages.
9
+pub const I3_MAGIC: &[u8; 6] = b"i3-ipc";
10
+
11
+/// Header size: 6 (magic) + 4 (length) + 4 (type) = 14 bytes.
12
+pub const HEADER_SIZE: usize = 14;
13
+
14
+/// High bit mask for event messages.
15
+pub const EVENT_MASK: u32 = 0x80000000;
16
+
17
+/// i3 IPC message types (requests).
18
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19
+#[repr(u32)]
20
+pub enum MessageType {
21
+    RunCommand = 0,
22
+    GetWorkspaces = 1,
23
+    Subscribe = 2,
24
+    GetOutputs = 3,
25
+    GetTree = 4,
26
+    GetMarks = 5,
27
+    GetBarConfig = 6,
28
+    GetVersion = 7,
29
+    GetBindingModes = 8,
30
+    GetConfig = 9,
31
+    SendTick = 10,
32
+    Sync = 11,
33
+    GetBindingState = 12,
34
+}
35
+
36
+impl MessageType {
37
+    pub fn from_u32(val: u32) -> Option<Self> {
38
+        match val {
39
+            0 => Some(Self::RunCommand),
40
+            1 => Some(Self::GetWorkspaces),
41
+            2 => Some(Self::Subscribe),
42
+            3 => Some(Self::GetOutputs),
43
+            4 => Some(Self::GetTree),
44
+            5 => Some(Self::GetMarks),
45
+            6 => Some(Self::GetBarConfig),
46
+            7 => Some(Self::GetVersion),
47
+            8 => Some(Self::GetBindingModes),
48
+            9 => Some(Self::GetConfig),
49
+            10 => Some(Self::SendTick),
50
+            11 => Some(Self::Sync),
51
+            12 => Some(Self::GetBindingState),
52
+            _ => None,
53
+        }
54
+    }
55
+}
56
+
57
+/// i3 IPC event types (responses with high bit set).
58
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59
+#[repr(u32)]
60
+pub enum EventType {
61
+    Workspace = 0,
62
+    Output = 1,
63
+    Mode = 2,
64
+    Window = 3,
65
+    BarconfigUpdate = 4,
66
+    Binding = 5,
67
+    Shutdown = 6,
68
+    Tick = 7,
69
+}
70
+
71
+impl EventType {
72
+    /// Get the wire format value (with high bit set).
73
+    pub fn to_wire(&self) -> u32 {
74
+        EVENT_MASK | (*self as u32)
75
+    }
76
+}
77
+
78
+/// A parsed i3 IPC message.
79
+#[derive(Debug)]
80
+pub struct I3Message {
81
+    pub msg_type: u32,
82
+    pub payload: Vec<u8>,
83
+}
84
+
85
+impl I3Message {
86
+    pub fn new(msg_type: u32, payload: Vec<u8>) -> Self {
87
+        Self { msg_type, payload }
88
+    }
89
+
90
+    /// Check if this is an event message (high bit set).
91
+    pub fn is_event(&self) -> bool {
92
+        self.msg_type & EVENT_MASK != 0
93
+    }
94
+
95
+    /// Get the message type without the event flag.
96
+    pub fn base_type(&self) -> u32 {
97
+        self.msg_type & !EVENT_MASK
98
+    }
99
+
100
+    /// Get payload as string (for JSON parsing).
101
+    pub fn payload_str(&self) -> Result<&str, std::str::Utf8Error> {
102
+        std::str::from_utf8(&self.payload)
103
+    }
104
+}
105
+
106
+/// Read an i3 IPC message from a stream.
107
+/// Returns None if the stream would block or is closed.
108
+pub fn read_message<R: Read>(reader: &mut R) -> io::Result<Option<I3Message>> {
109
+    // Read header
110
+    let mut header = [0u8; HEADER_SIZE];
111
+    match reader.read_exact(&mut header) {
112
+        Ok(_) => {}
113
+        Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(None),
114
+        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
115
+        Err(e) => return Err(e),
116
+    }
117
+
118
+    // Verify magic
119
+    if &header[0..6] != I3_MAGIC {
120
+        return Err(io::Error::new(
121
+            io::ErrorKind::InvalidData,
122
+            "Invalid i3 IPC magic",
123
+        ));
124
+    }
125
+
126
+    // Parse length and type (little-endian)
127
+    let length = u32::from_le_bytes([header[6], header[7], header[8], header[9]]) as usize;
128
+    let msg_type = u32::from_le_bytes([header[10], header[11], header[12], header[13]]);
129
+
130
+    // Sanity check length (max 16MB)
131
+    if length > 16 * 1024 * 1024 {
132
+        return Err(io::Error::new(
133
+            io::ErrorKind::InvalidData,
134
+            "Message too large",
135
+        ));
136
+    }
137
+
138
+    // Read payload
139
+    let mut payload = vec![0u8; length];
140
+    if length > 0 {
141
+        reader.read_exact(&mut payload)?;
142
+    }
143
+
144
+    Ok(Some(I3Message::new(msg_type, payload)))
145
+}
146
+
147
+/// Write an i3 IPC message to a stream.
148
+pub fn write_message<W: Write>(writer: &mut W, msg_type: u32, payload: &[u8]) -> io::Result<()> {
149
+    let length = payload.len() as u32;
150
+
151
+    // Write header
152
+    writer.write_all(I3_MAGIC)?;
153
+    writer.write_all(&length.to_le_bytes())?;
154
+    writer.write_all(&msg_type.to_le_bytes())?;
155
+
156
+    // Write payload
157
+    if !payload.is_empty() {
158
+        writer.write_all(payload)?;
159
+    }
160
+
161
+    writer.flush()
162
+}
163
+
164
+/// Write an i3 IPC response (same type as request).
165
+pub fn write_response<W: Write>(writer: &mut W, msg_type: u32, json: &str) -> io::Result<()> {
166
+    write_message(writer, msg_type, json.as_bytes())
167
+}
168
+
169
+/// Write an i3 IPC event (with high bit set).
170
+pub fn write_event<W: Write>(writer: &mut W, event_type: EventType, json: &str) -> io::Result<()> {
171
+    write_message(writer, event_type.to_wire(), json.as_bytes())
172
+}
173
+
174
+#[cfg(test)]
175
+mod tests {
176
+    use super::*;
177
+    use std::io::Cursor;
178
+
179
+    #[test]
180
+    fn test_write_read_roundtrip() {
181
+        let mut buf = Vec::new();
182
+        write_message(&mut buf, 1, b"{\"test\": true}").unwrap();
183
+
184
+        let mut cursor = Cursor::new(buf);
185
+        let msg = read_message(&mut cursor).unwrap().unwrap();
186
+
187
+        assert_eq!(msg.msg_type, 1);
188
+        assert_eq!(msg.payload_str().unwrap(), "{\"test\": true}");
189
+    }
190
+
191
+    #[test]
192
+    fn test_event_type_wire_format() {
193
+        assert_eq!(EventType::Workspace.to_wire(), 0x80000000);
194
+        assert_eq!(EventType::Output.to_wire(), 0x80000001);
195
+    }
196
+}
gar/src/ipc/i3_server.rsadded
@@ -0,0 +1,337 @@
1
+//! i3-compatible IPC server for polybar and other i3 ecosystem tools.
2
+//!
3
+//! Implements a subset of i3's IPC protocol:
4
+//! - GET_WORKSPACES (type 1)
5
+//! - SUBSCRIBE (type 2)
6
+//! - GET_OUTPUTS (type 3)
7
+//! - GET_VERSION (type 7)
8
+//!
9
+//! Socket path is set via I3SOCK environment variable.
10
+
11
+use std::collections::HashSet;
12
+use std::io::{BufReader, BufWriter};
13
+use std::os::unix::net::{UnixListener, UnixStream};
14
+use std::path::PathBuf;
15
+
16
+use super::i3_compat::{read_message, write_event, write_response, EventType, I3Message};
17
+
18
+/// Result of reading from a client.
19
+enum ReadResult {
20
+    Message(I3Message),
21
+    WouldBlock,
22
+    Disconnected,
23
+}
24
+
25
+/// A connected i3 IPC client.
26
+struct I3Client {
27
+    stream: UnixStream,
28
+    reader: BufReader<UnixStream>,
29
+    writer: BufWriter<UnixStream>,
30
+    subscriptions: HashSet<String>,
31
+}
32
+
33
+impl I3Client {
34
+    fn new(stream: UnixStream) -> std::io::Result<Self> {
35
+        stream.set_nonblocking(true)?;
36
+        let reader = BufReader::new(stream.try_clone()?);
37
+        let writer = BufWriter::new(stream.try_clone()?);
38
+        Ok(Self {
39
+            stream,
40
+            reader,
41
+            writer,
42
+            subscriptions: HashSet::new(),
43
+        })
44
+    }
45
+
46
+    fn read_message(&mut self) -> ReadResult {
47
+        match read_message(&mut self.reader) {
48
+            Ok(Some(msg)) => ReadResult::Message(msg),
49
+            Ok(None) => ReadResult::WouldBlock,
50
+            Err(e) => {
51
+                tracing::debug!("i3 IPC client read error: {}", e);
52
+                ReadResult::Disconnected
53
+            }
54
+        }
55
+    }
56
+
57
+    fn send_response(&mut self, msg_type: u32, json: &str) -> std::io::Result<()> {
58
+        write_response(&mut self.writer, msg_type, json)
59
+    }
60
+
61
+    fn send_event(&mut self, event_type: EventType, json: &str) -> std::io::Result<()> {
62
+        write_event(&mut self.writer, event_type, json)
63
+    }
64
+
65
+    fn is_subscribed(&self, event: &str) -> bool {
66
+        self.subscriptions.contains(event)
67
+    }
68
+
69
+    fn subscribe(&mut self, events: Vec<String>) {
70
+        for event in events {
71
+            self.subscriptions.insert(event);
72
+        }
73
+    }
74
+}
75
+
76
+/// i3-compatible IPC server.
77
+pub struct I3IpcServer {
78
+    listener: UnixListener,
79
+    clients: Vec<I3Client>,
80
+    socket_path: PathBuf,
81
+}
82
+
83
+impl I3IpcServer {
84
+    /// Create a new i3-compatible IPC server.
85
+    /// Sets the I3SOCK environment variable for client discovery.
86
+    pub fn new() -> std::io::Result<Self> {
87
+        let socket_path = Self::socket_path();
88
+
89
+        // Remove existing socket
90
+        let _ = std::fs::remove_file(&socket_path);
91
+
92
+        // Create parent directory if needed
93
+        if let Some(parent) = socket_path.parent() {
94
+            std::fs::create_dir_all(parent)?;
95
+        }
96
+
97
+        let listener = UnixListener::bind(&socket_path)?;
98
+        listener.set_nonblocking(true)?;
99
+
100
+        // Set socket permissions to user-only
101
+        #[cfg(unix)]
102
+        {
103
+            use std::os::unix::fs::PermissionsExt;
104
+            std::fs::set_permissions(&socket_path, std::fs::Permissions::from_mode(0o600))?;
105
+        }
106
+
107
+        // Set I3SOCK environment variable so polybar and other tools can find us
108
+        // SAFETY: We're setting this at startup before any threads are spawned
109
+        unsafe { std::env::set_var("I3SOCK", &socket_path); }
110
+
111
+        tracing::info!("i3-compatible IPC server listening on {:?}", socket_path);
112
+        tracing::info!("I3SOCK={}", socket_path.display());
113
+
114
+        Ok(Self {
115
+            listener,
116
+            clients: Vec::new(),
117
+            socket_path,
118
+        })
119
+    }
120
+
121
+    /// Get the socket path.
122
+    fn socket_path() -> PathBuf {
123
+        std::env::var("XDG_RUNTIME_DIR")
124
+            .map(|dir| PathBuf::from(dir).join("gar-i3.sock"))
125
+            .unwrap_or_else(|_| PathBuf::from("/tmp/gar-i3.sock"))
126
+    }
127
+
128
+    /// Accept new connections (non-blocking).
129
+    pub fn accept_connections(&mut self) {
130
+        loop {
131
+            match self.listener.accept() {
132
+                Ok((stream, _addr)) => {
133
+                    tracing::debug!("New i3 IPC client connected");
134
+                    match I3Client::new(stream) {
135
+                        Ok(client) => self.clients.push(client),
136
+                        Err(e) => tracing::warn!("Failed to setup i3 IPC client: {}", e),
137
+                    }
138
+                }
139
+                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
140
+                Err(e) => {
141
+                    tracing::warn!("Failed to accept i3 IPC connection: {}", e);
142
+                    break;
143
+                }
144
+            }
145
+        }
146
+    }
147
+
148
+    /// Process incoming requests from all clients.
149
+    /// Returns a list of (client_index, message) pairs.
150
+    pub fn poll_requests(&mut self) -> Vec<(usize, I3Message)> {
151
+        let mut requests = Vec::new();
152
+        let mut disconnected = Vec::new();
153
+
154
+        for (i, client) in self.clients.iter_mut().enumerate() {
155
+            match client.read_message() {
156
+                ReadResult::Message(msg) => requests.push((i, msg)),
157
+                ReadResult::Disconnected => disconnected.push(i),
158
+                ReadResult::WouldBlock => {}
159
+            }
160
+        }
161
+
162
+        // Remove disconnected clients (in reverse order to preserve indices)
163
+        for i in disconnected.into_iter().rev() {
164
+            tracing::debug!("i3 IPC client disconnected");
165
+            self.clients.remove(i);
166
+        }
167
+
168
+        requests
169
+    }
170
+
171
+    /// Send a response to a specific client.
172
+    pub fn send_response(&mut self, client_idx: usize, msg_type: u32, json: &str) {
173
+        if let Some(client) = self.clients.get_mut(client_idx) {
174
+            if let Err(e) = client.send_response(msg_type, json) {
175
+                tracing::warn!("Failed to send i3 IPC response: {}", e);
176
+            }
177
+        }
178
+    }
179
+
180
+    /// Subscribe a client to events.
181
+    pub fn subscribe(&mut self, client_idx: usize, events: Vec<String>) {
182
+        if let Some(client) = self.clients.get_mut(client_idx) {
183
+            client.subscribe(events);
184
+        }
185
+    }
186
+
187
+    /// Broadcast a workspace event to all subscribed clients.
188
+    pub fn broadcast_workspace_event(&mut self, json: &str) {
189
+        let mut disconnected = Vec::new();
190
+
191
+        for (i, client) in self.clients.iter_mut().enumerate() {
192
+            if client.is_subscribed("workspace") {
193
+                if client.send_event(EventType::Workspace, json).is_err() {
194
+                    disconnected.push(i);
195
+                }
196
+            }
197
+        }
198
+
199
+        // Remove failed clients
200
+        for i in disconnected.into_iter().rev() {
201
+            self.clients.remove(i);
202
+        }
203
+    }
204
+
205
+    /// Broadcast an output event to all subscribed clients.
206
+    pub fn broadcast_output_event(&mut self, json: &str) {
207
+        let mut disconnected = Vec::new();
208
+
209
+        for (i, client) in self.clients.iter_mut().enumerate() {
210
+            if client.is_subscribed("output") {
211
+                if client.send_event(EventType::Output, json).is_err() {
212
+                    disconnected.push(i);
213
+                }
214
+            }
215
+        }
216
+
217
+        // Remove failed clients
218
+        for i in disconnected.into_iter().rev() {
219
+            self.clients.remove(i);
220
+        }
221
+    }
222
+
223
+    /// Get client count.
224
+    pub fn client_count(&self) -> usize {
225
+        self.clients.len()
226
+    }
227
+}
228
+
229
+impl Drop for I3IpcServer {
230
+    fn drop(&mut self) {
231
+        // Clean up socket file
232
+        let _ = std::fs::remove_file(&self.socket_path);
233
+        // Clear I3SOCK env var
234
+        // SAFETY: We're removing this during cleanup, single-threaded context
235
+        unsafe { std::env::remove_var("I3SOCK"); }
236
+        tracing::debug!("i3 IPC server shutdown, socket removed");
237
+    }
238
+}
239
+
240
+/// Build GET_WORKSPACES response JSON.
241
+pub fn build_workspaces_json(workspaces: &[WorkspaceInfo]) -> String {
242
+    serde_json::to_string(workspaces).unwrap_or_else(|_| "[]".to_string())
243
+}
244
+
245
+/// Build GET_OUTPUTS response JSON.
246
+pub fn build_outputs_json(outputs: &[OutputInfo]) -> String {
247
+    serde_json::to_string(outputs).unwrap_or_else(|_| "[]".to_string())
248
+}
249
+
250
+/// Build workspace event JSON.
251
+pub fn build_workspace_event_json(change: &str, current: &WorkspaceInfo, old: Option<&WorkspaceInfo>) -> String {
252
+    let event = WorkspaceEvent {
253
+        change: change.to_string(),
254
+        current: current.clone(),
255
+        old: old.cloned(),
256
+    };
257
+    serde_json::to_string(&event).unwrap_or_else(|_| r#"{"change":"focus"}"#.to_string())
258
+}
259
+
260
+/// Build output event JSON.
261
+pub fn build_output_event_json() -> String {
262
+    r#"{"change":"unspecified"}"#.to_string()
263
+}
264
+
265
+/// Build GET_VERSION response JSON.
266
+pub fn build_version_json() -> String {
267
+    let version = VersionInfo {
268
+        major: 0,
269
+        minor: 1,
270
+        patch: 0,
271
+        human_readable: "gar 0.1.0 (i3-compat)".to_string(),
272
+        loaded_config_file_name: "~/.config/gar/init.lua".to_string(),
273
+    };
274
+    serde_json::to_string(&version).unwrap_or_else(|_| r#"{"human_readable":"gar"}"#.to_string())
275
+}
276
+
277
+/// Build SUBSCRIBE success response JSON.
278
+pub fn build_subscribe_success_json() -> String {
279
+    r#"{"success":true}"#.to_string()
280
+}
281
+
282
+// ============================================================================
283
+// Data structures for JSON serialization (i3-compatible)
284
+// ============================================================================
285
+
286
+use serde::{Deserialize, Serialize};
287
+
288
+/// Workspace info for GET_WORKSPACES response.
289
+#[derive(Debug, Clone, Serialize, Deserialize)]
290
+pub struct WorkspaceInfo {
291
+    pub id: i64,
292
+    pub num: i32,
293
+    pub name: String,
294
+    pub visible: bool,
295
+    pub focused: bool,
296
+    pub urgent: bool,
297
+    pub rect: Rect,
298
+    pub output: String,
299
+}
300
+
301
+/// Output info for GET_OUTPUTS response.
302
+#[derive(Debug, Clone, Serialize, Deserialize)]
303
+pub struct OutputInfo {
304
+    pub name: String,
305
+    pub active: bool,
306
+    pub primary: bool,
307
+    pub current_workspace: Option<String>,
308
+    pub rect: Rect,
309
+}
310
+
311
+/// Rectangle for geometry info.
312
+#[derive(Debug, Clone, Serialize, Deserialize)]
313
+pub struct Rect {
314
+    pub x: i32,
315
+    pub y: i32,
316
+    pub width: i32,
317
+    pub height: i32,
318
+}
319
+
320
+/// Version info for GET_VERSION response.
321
+#[derive(Debug, Clone, Serialize, Deserialize)]
322
+pub struct VersionInfo {
323
+    pub major: i32,
324
+    pub minor: i32,
325
+    pub patch: i32,
326
+    pub human_readable: String,
327
+    pub loaded_config_file_name: String,
328
+}
329
+
330
+/// Workspace event payload.
331
+#[derive(Debug, Clone, Serialize, Deserialize)]
332
+pub struct WorkspaceEvent {
333
+    pub change: String,
334
+    pub current: WorkspaceInfo,
335
+    #[serde(skip_serializing_if = "Option::is_none")]
336
+    pub old: Option<WorkspaceInfo>,
337
+}
gar/src/ipc/mod.rsmodified
@@ -1,5 +1,8 @@
11
 mod protocol;
22
 mod server;
3
+pub mod i3_compat;
4
+pub mod i3_server;
35
 
46
 pub use protocol::{Event, Request, Response, WindowInfo, WorkspaceInfo};
57
 pub use server::IpcServer;
8
+pub use i3_server::{I3IpcServer, WorkspaceInfo as I3WorkspaceInfo, OutputInfo, Rect as I3Rect};
gar/src/x11/events.rsmodified
@@ -178,10 +178,12 @@ impl WindowManager {
178178
             Event::RandrScreenChangeNotify(_) => {
179179
                 tracing::info!("RandR screen change detected, refreshing monitors");
180180
                 self.refresh_monitors()?;
181
+                self.broadcast_i3_output_event();
181182
             }
182183
             Event::RandrNotify(_) => {
183184
                 tracing::info!("RandR notify event, refreshing monitors");
184185
                 self.refresh_monitors()?;
186
+                self.broadcast_i3_output_event();
185187
             }
186188
             Event::ClientMessage(e) => {
187189
                 self.handle_client_message(e)?;
@@ -967,6 +969,9 @@ impl WindowManager {
967969
             return Ok(());
968970
         }
969971
 
972
+        // Track old workspace for event broadcasting
973
+        let old_workspace_idx = self.focused_workspace;
974
+
970975
         // Check if workspace is already visible on some monitor
971976
         let visible_on_monitor = self.monitors.iter().position(|m| m.active_workspace == idx);
972977
 
@@ -1061,6 +1066,11 @@ impl WindowManager {
10611066
             }
10621067
         }
10631068
 
1069
+        // Broadcast i3 workspace event for polybar
1070
+        if idx != old_workspace_idx {
1071
+            self.broadcast_i3_workspace_event("focus", idx, Some(old_workspace_idx));
1072
+        }
1073
+
10641074
         self.conn.flush()?;
10651075
         Ok(())
10661076
     }
@@ -1214,6 +1224,9 @@ impl WindowManager {
12141224
             // Handle IPC requests
12151225
             self.handle_ipc()?;
12161226
 
1227
+            // Handle i3-compatible IPC requests (for polybar)
1228
+            self.handle_i3_ipc()?;
1229
+
12171230
             // Small sleep to avoid busy-waiting when idle
12181231
             std::thread::sleep(std::time::Duration::from_millis(10));
12191232
         }
@@ -1737,6 +1750,187 @@ impl WindowManager {
17371750
 
17381751
         Ok(())
17391752
     }
1753
+
1754
+    // =========================================================================
1755
+    // i3-compatible IPC handling (for polybar integration)
1756
+    // =========================================================================
1757
+
1758
+    /// Handle pending i3-compatible IPC requests
1759
+    fn handle_i3_ipc(&mut self) -> Result<()> {
1760
+        use crate::ipc::i3_compat::MessageType;
1761
+        use crate::ipc::i3_server::{
1762
+            build_workspaces_json, build_outputs_json, build_version_json,
1763
+            build_subscribe_success_json,
1764
+        };
1765
+
1766
+        let Some(ref mut i3_ipc) = self.i3_ipc_server else {
1767
+            return Ok(());
1768
+        };
1769
+
1770
+        // Accept new connections
1771
+        i3_ipc.accept_connections();
1772
+
1773
+        // Process requests
1774
+        let requests = i3_ipc.poll_requests();
1775
+        for (client_idx, msg) in requests {
1776
+            let msg_type = msg.msg_type;
1777
+
1778
+            match MessageType::from_u32(msg_type) {
1779
+                Some(MessageType::GetWorkspaces) => {
1780
+                    let workspaces = self.build_i3_workspaces();
1781
+                    let json = build_workspaces_json(&workspaces);
1782
+                    if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1783
+                        i3_ipc.send_response(client_idx, msg_type, &json);
1784
+                    }
1785
+                }
1786
+                Some(MessageType::GetOutputs) => {
1787
+                    let outputs = self.build_i3_outputs();
1788
+                    let json = build_outputs_json(&outputs);
1789
+                    if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1790
+                        i3_ipc.send_response(client_idx, msg_type, &json);
1791
+                    }
1792
+                }
1793
+                Some(MessageType::Subscribe) => {
1794
+                    // Parse subscription request
1795
+                    if let Ok(events) = msg.payload_str() {
1796
+                        if let Ok(event_list) = serde_json::from_str::<Vec<String>>(events) {
1797
+                            if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1798
+                                i3_ipc.subscribe(client_idx, event_list);
1799
+                            }
1800
+                        }
1801
+                    }
1802
+                    let json = build_subscribe_success_json();
1803
+                    if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1804
+                        i3_ipc.send_response(client_idx, msg_type, &json);
1805
+                    }
1806
+                }
1807
+                Some(MessageType::GetVersion) => {
1808
+                    let json = build_version_json();
1809
+                    if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1810
+                        i3_ipc.send_response(client_idx, msg_type, &json);
1811
+                    }
1812
+                }
1813
+                Some(MessageType::RunCommand) => {
1814
+                    // Return success for now - we could parse and execute i3 commands later
1815
+                    let json = r#"[{"success":true}]"#;
1816
+                    if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1817
+                        i3_ipc.send_response(client_idx, msg_type, json);
1818
+                    }
1819
+                }
1820
+                _ => {
1821
+                    // Unknown or unsupported message type - return empty success
1822
+                    let json = r#"{"success":false,"error":"unsupported"}"#;
1823
+                    if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1824
+                        i3_ipc.send_response(client_idx, msg_type, json);
1825
+                    }
1826
+                }
1827
+            }
1828
+        }
1829
+
1830
+        Ok(())
1831
+    }
1832
+
1833
+    /// Build i3-compatible workspace list
1834
+    fn build_i3_workspaces(&self) -> Vec<crate::ipc::I3WorkspaceInfo> {
1835
+        use crate::ipc::{I3WorkspaceInfo, I3Rect};
1836
+
1837
+        self.workspaces.iter().enumerate().map(|(i, ws)| {
1838
+            // Find which monitor this workspace is on (if visible)
1839
+            let monitor = self.monitors.iter().find(|m| m.active_workspace == i);
1840
+            let visible = monitor.is_some();
1841
+            let focused = self.focused_monitor < self.monitors.len()
1842
+                && self.monitors[self.focused_monitor].active_workspace == i;
1843
+
1844
+            // Check if any window in this workspace is urgent
1845
+            let urgent = self.windows.values()
1846
+                .filter(|w| w.workspace == i)
1847
+                .any(|w| w.urgent);
1848
+
1849
+            // Get geometry from monitor if visible, else use first monitor's geometry
1850
+            let (rect, output) = if let Some(mon) = monitor {
1851
+                (
1852
+                    I3Rect {
1853
+                        x: mon.geometry.x as i32,
1854
+                        y: mon.geometry.y as i32,
1855
+                        width: mon.geometry.width as i32,
1856
+                        height: mon.geometry.height as i32,
1857
+                    },
1858
+                    mon.name.clone(),
1859
+                )
1860
+            } else {
1861
+                // Not visible - use first monitor as fallback
1862
+                let fallback = &self.monitors[0];
1863
+                (
1864
+                    I3Rect {
1865
+                        x: fallback.geometry.x as i32,
1866
+                        y: fallback.geometry.y as i32,
1867
+                        width: fallback.geometry.width as i32,
1868
+                        height: fallback.geometry.height as i32,
1869
+                    },
1870
+                    fallback.name.clone(),
1871
+                )
1872
+            };
1873
+
1874
+            I3WorkspaceInfo {
1875
+                id: (i + 1) as i64 * 1000000, // Generate unique ID
1876
+                num: (i + 1) as i32,
1877
+                name: ws.name.clone(),
1878
+                visible,
1879
+                focused,
1880
+                urgent,
1881
+                rect,
1882
+                output,
1883
+            }
1884
+        }).collect()
1885
+    }
1886
+
1887
+    /// Build i3-compatible output list
1888
+    fn build_i3_outputs(&self) -> Vec<crate::ipc::OutputInfo> {
1889
+        use crate::ipc::{OutputInfo, I3Rect};
1890
+
1891
+        self.monitors.iter().map(|mon| {
1892
+            let current_workspace = Some(self.workspaces[mon.active_workspace].name.clone());
1893
+
1894
+            OutputInfo {
1895
+                name: mon.name.clone(),
1896
+                active: true,
1897
+                primary: mon.primary,
1898
+                current_workspace,
1899
+                rect: I3Rect {
1900
+                    x: mon.geometry.x as i32,
1901
+                    y: mon.geometry.y as i32,
1902
+                    width: mon.geometry.width as i32,
1903
+                    height: mon.geometry.height as i32,
1904
+                },
1905
+            }
1906
+        }).collect()
1907
+    }
1908
+
1909
+    /// Broadcast i3 workspace event to subscribed clients
1910
+    pub fn broadcast_i3_workspace_event(&mut self, change: &str, workspace_idx: usize, old_workspace_idx: Option<usize>) {
1911
+        use crate::ipc::i3_server::build_workspace_event_json;
1912
+
1913
+        let workspaces = self.build_i3_workspaces();
1914
+        let current = workspaces.get(workspace_idx).cloned();
1915
+        let old = old_workspace_idx.and_then(|idx| workspaces.get(idx).cloned());
1916
+
1917
+        if let Some(current) = current {
1918
+            let json = build_workspace_event_json(change, &current, old.as_ref());
1919
+            if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1920
+                i3_ipc.broadcast_workspace_event(&json);
1921
+            }
1922
+        }
1923
+    }
1924
+
1925
+    /// Broadcast i3 output event to subscribed clients
1926
+    pub fn broadcast_i3_output_event(&mut self) {
1927
+        use crate::ipc::i3_server::build_output_event_json;
1928
+
1929
+        let json = build_output_event_json();
1930
+        if let Some(ref mut i3_ipc) = self.i3_ipc_server {
1931
+            i3_ipc.broadcast_output_event(&json);
1932
+        }
1933
+    }
17401934
 }
17411935
 
17421936
 fn parse_direction(s: &str) -> Option<Direction> {