sway_groups_core/sway/
client.rs1use std::io::{Read, Write};
4use std::os::unix::net::UnixStream;
5use std::path::Path;
6
7use super::types::*;
8use crate::error::{Error, Result};
9
10fn 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#[derive(Clone)]
30pub struct SwayIpcClient {
31 socket_path: String,
32}
33
34pub struct EventStream {
36 stream: UnixStream,
37}
38
39impl EventStream {
40 pub fn read_event(&mut self) -> Result<(u32, Vec<u8>)> {
43 read_ipc_frame(&mut self.stream)
44 }
45}
46
47impl SwayIpcClient {
48 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 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 fn connect(&self) -> Result<UnixStream> {
66 UnixStream::connect(&self.socket_path)
67 .map_err(|_| Error::SwayNotRunning)
68 }
69
70 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 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 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 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 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 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 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 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 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 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 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}