Skip to main content

sway_groups_core/sway/
client.rs

1//! Sway IPC client implementation.
2
3use std::io::{Read, Write};
4use std::os::unix::net::UnixStream;
5use std::path::Path;
6
7use super::types::*;
8use crate::error::{Error, Result};
9
10/// Read a complete IPC frame: header + payload.
11/// Shared by [`EventStream::read_event`] and [`SwayIpcClient`] message methods.
12fn read_ipc_frame(stream: &mut UnixStream) -> Result<(u32, Vec<u8>)> {
13    let mut header = [0u8; 14];
14    stream.read_exact(&mut header)?;
15
16    let ipc_header = IpcHeader::from_bytes(&header);
17
18    if &ipc_header.magic != b"i3-ipc" {
19        return Err(Error::SwayIpc("Invalid IPC magic".to_string()));
20    }
21
22    let mut payload = vec![0u8; ipc_header.payload_size as usize];
23    stream.read_exact(&mut payload)?;
24
25    Ok((ipc_header.message_type, payload))
26}
27
28/// Sway IPC client for communicating with sway.
29#[derive(Clone)]
30pub struct SwayIpcClient {
31    socket_path: String,
32}
33
34/// Stream for receiving sway IPC events after subscribing.
35pub struct EventStream {
36    stream: UnixStream,
37}
38
39impl EventStream {
40    /// Read the next event from the stream.
41    /// Returns the event type and payload.
42    pub fn read_event(&mut self) -> Result<(u32, Vec<u8>)> {
43        read_ipc_frame(&mut self.stream)
44    }
45}
46
47impl SwayIpcClient {
48    /// Create a new sway IPC client.
49    /// Uses the SWAYSOCK environment variable to find the socket.
50    pub fn new() -> Result<Self> {
51        let socket_path = std::env::var("SWAYSOCK")
52            .map_err(|_| Error::SwayNotRunning)?;
53
54        Ok(Self { socket_path })
55    }
56
57    /// Create with a specific socket path.
58    pub fn with_path<P: AsRef<Path>>(socket_path: P) -> Self {
59        Self {
60            socket_path: socket_path.as_ref().to_string_lossy().to_string(),
61        }
62    }
63
64    /// Connect to sway and return a stream.
65    fn connect(&self) -> Result<UnixStream> {
66        UnixStream::connect(&self.socket_path)
67            .map_err(|_| Error::SwayNotRunning)
68    }
69
70    /// Subscribe to sway events. Returns an EventStream that yields events.
71    /// Events: "workspace", "output", "mode", "window", "barconfig_update", "binding", "shutdown", "tick"
72    pub fn subscribe(&self, events: &[&str]) -> Result<EventStream> {
73        let mut stream = self.connect()?;
74
75        let payload = serde_json::to_string(events)?;
76        let header = IpcHeader::new(SwayMsgType::Subscribe, payload.len() as u32);
77
78        stream.write_all(&header.to_bytes())?;
79        stream.write_all(payload.as_bytes())?;
80        stream.flush()?;
81
82        let response = Self::read_message(&mut stream)?;
83        let result: serde_json::Value = serde_json::from_slice(&response)?;
84        if result.get("success").and_then(|v| v.as_bool()) != Some(true) {
85            return Err(Error::SwayIpc("Failed to subscribe to sway events".to_string()));
86        }
87
88        Ok(EventStream { stream })
89    }
90
91    /// Send a command to sway and get the result.
92    pub fn run_command(&self, command: &str) -> Result<Vec<CommandResult>> {
93        let mut stream = self.connect()?;
94
95        let payload = command.as_bytes();
96        let header = IpcHeader::new(SwayMsgType::RunCommand, payload.len() as u32);
97
98        stream.write_all(&header.to_bytes())?;
99        stream.write_all(payload)?;
100        stream.flush()?;
101
102        let response = Self::read_message(&mut stream)?;
103
104        let results: Vec<CommandResult> = serde_json::from_slice(&response)?;
105        Ok(results)
106    }
107
108    /// Get all workspaces.
109    pub fn get_workspaces(&self) -> Result<Vec<SwayWorkspace>> {
110        let mut stream = self.connect()?;
111
112        let header = IpcHeader::new(SwayMsgType::GetWorkspaces, 0);
113
114        stream.write_all(&header.to_bytes())?;
115        stream.flush()?;
116
117        let response = Self::read_message(&mut stream)?;
118
119        let workspaces: Vec<SwayWorkspace> = serde_json::from_slice(&response)?;
120        Ok(workspaces)
121    }
122
123    /// Get all outputs.
124    pub fn get_outputs(&self) -> Result<Vec<SwayOutput>> {
125        let mut stream = self.connect()?;
126
127        let header = IpcHeader::new(SwayMsgType::GetOutputs, 0);
128
129        stream.write_all(&header.to_bytes())?;
130        stream.flush()?;
131
132        let response = Self::read_message(&mut stream)?;
133
134        let outputs: Vec<SwayOutput> = serde_json::from_slice(&response)?;
135        Ok(outputs)
136    }
137
138    /// Get the focused workspace.
139    pub fn get_focused_workspace(&self) -> Result<SwayWorkspace> {
140        let workspaces = self.get_workspaces()?;
141        workspaces
142            .into_iter()
143            .find(|w| w.focused)
144            .ok_or_else(|| Error::SwayIpc("No focused workspace".to_string()))
145    }
146
147    /// Check if the focused workspace is empty (no windows/containers).
148    pub fn is_focused_workspace_empty(&self) -> Result<bool> {
149        let ws = self.get_focused_workspace()?;
150        Ok(ws.representation.is_none())
151    }
152
153    /// Rename a workspace.
154    pub fn rename_workspace(&self, old_name: &str, new_name: &str) -> Result<()> {
155        let command = format!("rename workspace \"{}\" to \"{}\"", old_name, new_name);
156        let results = self.run_command(&command)?;
157
158        if let Some(result) = results.first() {
159            if result.success {
160                Ok(())
161            } else {
162                Err(Error::SwayIpc(
163                    result.error.clone().unwrap_or_else(|| "Unknown error".to_string()),
164                ))
165            }
166        } else {
167            Err(Error::SwayIpc("Empty response".to_string()))
168        }
169    }
170
171    /// Get the sway tree (for introspecting containers, windows, etc.)
172    pub fn get_tree(&self) -> Result<Vec<u8>> {
173        let mut stream = self.connect()?;
174
175        let header = IpcHeader::new(SwayMsgType::GetTree, 0);
176
177        stream.write_all(&header.to_bytes())?;
178        stream.flush()?;
179
180        let response = Self::read_message(&mut stream)?;
181        Ok(response)
182    }
183
184    /// Get current workspace names.
185    pub fn get_workspace_names(&self) -> Result<Vec<String>> {
186        let workspaces = self.get_workspaces()?;
187        Ok(workspaces.into_iter().map(|w| w.name).collect())
188    }
189
190    /// Get the primary output (output of the currently focused workspace).
191    /// Falls back to the first available output.
192    pub fn get_primary_output(&self) -> Result<String> {
193        if let Ok(focused) = self.get_focused_workspace() {
194            return Ok(focused.output);
195        }
196        let outputs = self.get_outputs()?;
197        outputs
198            .into_iter()
199            .next()
200            .map(|o| o.name)
201            .ok_or_else(|| Error::SwayIpc("No outputs available".to_string()))
202    }
203
204    /// Read a message payload from the stream.
205    fn read_message(stream: &mut UnixStream) -> Result<Vec<u8>> {
206        read_ipc_frame(stream).map(|(_, payload)| payload)
207    }
208}
209
210impl Default for SwayIpcClient {
211    fn default() -> Self {
212        Self::new().expect("SWAYSOCK not set")
213    }
214}