orchflow_mux/
muxd_backend.rs

1//! Muxd backend implementation - a modern terminal multiplexer daemon
2
3use crate::backend::{MuxBackend, MuxError, Pane, PaneSize, Session, SplitType};
4use async_trait::async_trait;
5use chrono::Utc;
6use futures_util::{SinkExt, StreamExt};
7use serde_json::{json, Value};
8use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tokio_tungstenite::connect_async;
12use tracing::{debug, info};
13
14/// MuxdBackend connects to the muxd daemon via WebSocket
15pub struct MuxdBackend {
16    url: String,
17    sessions: Arc<RwLock<HashMap<String, Session>>>,
18    panes: Arc<RwLock<HashMap<String, Pane>>>,
19}
20
21impl MuxdBackend {
22    pub fn new(url: String) -> Self {
23        Self {
24            url,
25            sessions: Arc::new(RwLock::new(HashMap::new())),
26            panes: Arc::new(RwLock::new(HashMap::new())),
27        }
28    }
29
30    /// Connect to muxd and send a command
31    async fn send_command(&self, command: Value) -> Result<Value, MuxError> {
32        let (mut ws, _) = connect_async(&self.url)
33            .await
34            .map_err(|e| MuxError::ConnectionError(format!("Failed to connect to muxd: {e}")))?;
35
36        // Send command
37        ws.send(tokio_tungstenite::tungstenite::Message::Text(
38            command.to_string().into(),
39        ))
40        .await
41        .map_err(|e| MuxError::ConnectionError(format!("Failed to send command: {e}")))?;
42
43        // Wait for response
44        if let Some(Ok(msg)) = ws.next().await {
45            match msg {
46                tokio_tungstenite::tungstenite::Message::Text(text) => serde_json::from_str(&text)
47                    .map_err(|e| MuxError::ParseError(format!("Invalid response: {e}"))),
48                _ => Err(MuxError::ParseError("Expected text response".to_string())),
49            }
50        } else {
51            Err(MuxError::ConnectionError(
52                "No response from muxd".to_string(),
53            ))
54        }
55    }
56}
57
58#[async_trait]
59impl MuxBackend for MuxdBackend {
60    async fn create_session(&self, name: &str) -> Result<String, MuxError> {
61        info!("Creating muxd session: {}", name);
62
63        let command = json!({
64            "type": "create_session",
65            "name": name
66        });
67
68        let response = self.send_command(command).await?;
69
70        if let Some(session_id) = response.get("session_id").and_then(|v| v.as_str()) {
71            let session = Session {
72                id: session_id.to_string(),
73                name: name.to_string(),
74                created_at: Utc::now(),
75                window_count: 1,
76                attached: false,
77            };
78
79            self.sessions
80                .write()
81                .await
82                .insert(session_id.to_string(), session);
83            Ok(session_id.to_string())
84        } else {
85            Err(MuxError::ParseError(
86                "Invalid create_session response".to_string(),
87            ))
88        }
89    }
90
91    async fn create_pane(&self, session_id: &str, split: SplitType) -> Result<String, MuxError> {
92        debug!(
93            "Creating pane in muxd session: {} with split: {:?}",
94            session_id, split
95        );
96
97        let command = json!({
98            "type": "create_pane",
99            "session_id": session_id,
100            "split": match split {
101                SplitType::Horizontal => "horizontal",
102                SplitType::Vertical => "vertical",
103                SplitType::None => "none",
104            }
105        });
106
107        let response = self.send_command(command).await?;
108
109        if let Some(pane_id) = response.get("pane_id").and_then(|v| v.as_str()) {
110            let pane = Pane {
111                id: pane_id.to_string(),
112                session_id: session_id.to_string(),
113                index: 0,
114                title: String::new(),
115                active: true,
116                size: PaneSize {
117                    width: 80,
118                    height: 24,
119                },
120            };
121
122            self.panes.write().await.insert(pane_id.to_string(), pane);
123            Ok(pane_id.to_string())
124        } else {
125            Err(MuxError::ParseError(
126                "Invalid create_pane response".to_string(),
127            ))
128        }
129    }
130
131    async fn send_keys(&self, pane_id: &str, keys: &str) -> Result<(), MuxError> {
132        debug!("Sending keys to muxd pane {}: {}", pane_id, keys);
133
134        let command = json!({
135            "type": "send_keys",
136            "pane_id": pane_id,
137            "keys": keys
138        });
139
140        self.send_command(command).await?;
141        Ok(())
142    }
143
144    async fn capture_pane(&self, pane_id: &str) -> Result<String, MuxError> {
145        debug!("Capturing muxd pane: {}", pane_id);
146
147        let command = json!({
148            "type": "capture_pane",
149            "pane_id": pane_id
150        });
151
152        let response = self.send_command(command).await?;
153
154        response
155            .get("content")
156            .and_then(|v| v.as_str())
157            .map(|s| s.to_string())
158            .ok_or_else(|| MuxError::ParseError("Invalid capture_pane response".to_string()))
159    }
160
161    async fn list_sessions(&self) -> Result<Vec<Session>, MuxError> {
162        debug!("Listing muxd sessions");
163
164        let command = json!({
165            "type": "list_sessions"
166        });
167
168        let response = self.send_command(command).await?;
169
170        if let Some(sessions_data) = response.get("sessions").and_then(|v| v.as_array()) {
171            let sessions = sessions_data
172                .iter()
173                .filter_map(|v| {
174                    let id = v.get("id")?.as_str()?;
175                    let name = v.get("name")?.as_str()?;
176                    Some(Session {
177                        id: id.to_string(),
178                        name: name.to_string(),
179                        created_at: Utc::now(),
180                        window_count: v.get("window_count").and_then(|v| v.as_u64()).unwrap_or(0)
181                            as usize,
182                        attached: v.get("attached").and_then(|v| v.as_bool()).unwrap_or(false),
183                    })
184                })
185                .collect();
186            Ok(sessions)
187        } else {
188            Err(MuxError::ParseError(
189                "Invalid list_sessions response".to_string(),
190            ))
191        }
192    }
193
194    async fn kill_session(&self, session_id: &str) -> Result<(), MuxError> {
195        info!("Killing muxd session: {}", session_id);
196
197        let command = json!({
198            "type": "kill_session",
199            "session_id": session_id
200        });
201
202        self.send_command(command).await?;
203        self.sessions.write().await.remove(session_id);
204        Ok(())
205    }
206
207    async fn kill_pane(&self, pane_id: &str) -> Result<(), MuxError> {
208        debug!("Killing muxd pane: {}", pane_id);
209
210        let command = json!({
211            "type": "kill_pane",
212            "pane_id": pane_id
213        });
214
215        self.send_command(command).await?;
216        self.panes.write().await.remove(pane_id);
217        Ok(())
218    }
219
220    async fn resize_pane(&self, pane_id: &str, size: PaneSize) -> Result<(), MuxError> {
221        debug!(
222            "Resizing muxd pane {} to {}x{}",
223            pane_id, size.width, size.height
224        );
225
226        let command = json!({
227            "type": "resize_pane",
228            "pane_id": pane_id,
229            "width": size.width,
230            "height": size.height
231        });
232
233        self.send_command(command).await?;
234
235        if let Some(pane) = self.panes.write().await.get_mut(pane_id) {
236            pane.size = size;
237        }
238
239        Ok(())
240    }
241
242    async fn select_pane(&self, pane_id: &str) -> Result<(), MuxError> {
243        debug!("Selecting muxd pane: {}", pane_id);
244
245        let command = json!({
246            "type": "select_pane",
247            "pane_id": pane_id
248        });
249
250        self.send_command(command).await?;
251        Ok(())
252    }
253
254    async fn list_panes(&self, session_id: &str) -> Result<Vec<Pane>, MuxError> {
255        debug!("Listing panes for muxd session: {}", session_id);
256
257        let command = json!({
258            "type": "list_panes",
259            "session_id": session_id
260        });
261
262        let response = self.send_command(command).await?;
263
264        if let Some(panes_data) = response.get("panes").and_then(|v| v.as_array()) {
265            let panes = panes_data
266                .iter()
267                .filter_map(|v| {
268                    let id = v.get("id")?.as_str()?;
269                    let index = v.get("index")?.as_u64()? as u32;
270                    Some(Pane {
271                        id: id.to_string(),
272                        session_id: session_id.to_string(),
273                        index,
274                        title: v
275                            .get("title")
276                            .and_then(|v| v.as_str())
277                            .unwrap_or("")
278                            .to_string(),
279                        active: v.get("active").and_then(|v| v.as_bool()).unwrap_or(false),
280                        size: PaneSize {
281                            width: v.get("width").and_then(|v| v.as_u64()).unwrap_or(80) as u32,
282                            height: v.get("height").and_then(|v| v.as_u64()).unwrap_or(24) as u32,
283                        },
284                    })
285                })
286                .collect();
287            Ok(panes)
288        } else {
289            Err(MuxError::ParseError(
290                "Invalid list_panes response".to_string(),
291            ))
292        }
293    }
294
295    async fn attach_session(&self, session_id: &str) -> Result<(), MuxError> {
296        info!("Attaching to muxd session: {}", session_id);
297
298        let command = json!({
299            "type": "attach_session",
300            "session_id": session_id
301        });
302
303        self.send_command(command).await?;
304
305        if let Some(session) = self.sessions.write().await.get_mut(session_id) {
306            session.attached = true;
307        }
308
309        Ok(())
310    }
311
312    async fn detach_session(&self, session_id: &str) -> Result<(), MuxError> {
313        info!("Detaching from muxd session: {}", session_id);
314
315        let command = json!({
316            "type": "detach_session",
317            "session_id": session_id
318        });
319
320        self.send_command(command).await?;
321
322        if let Some(session) = self.sessions.write().await.get_mut(session_id) {
323            session.attached = false;
324        }
325
326        Ok(())
327    }
328}