1use crate::backend::{MuxBackend, MuxError, Pane, PaneSize, Session, SplitType};
4use async_trait::async_trait;
5use chrono::Utc;
6use std::process::Command;
7use tracing::{debug, info};
8
9#[derive(Clone, Default)]
11pub struct TmuxBackend;
12
13impl TmuxBackend {
14 pub fn new() -> Self {
15 Self
16 }
17
18 async fn tmux_command(&self, args: &[&str]) -> Result<String, MuxError> {
20 let output = tokio::task::spawn_blocking({
21 let args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
22 move || Command::new("tmux").args(&args).output()
23 })
24 .await
25 .map_err(|e| MuxError::Other(format!("Failed to spawn tmux command: {e}")))?
26 .map_err(|e| MuxError::CommandFailed(format!("Failed to execute tmux: {e}")))?;
27
28 if !output.status.success() {
29 let stderr = String::from_utf8_lossy(&output.stderr);
30 return Err(MuxError::CommandFailed(format!(
31 "tmux command failed: {stderr}"
32 )));
33 }
34
35 Ok(String::from_utf8_lossy(&output.stdout).to_string())
36 }
37}
38
39#[async_trait]
40impl MuxBackend for TmuxBackend {
41 async fn create_session(&self, name: &str) -> Result<String, MuxError> {
42 info!("Creating tmux session: {}", name);
43
44 self.tmux_command(&["new-session", "-d", "-s", name])
46 .await?;
47
48 Ok(name.to_string())
49 }
50
51 async fn create_pane(&self, session_id: &str, split: SplitType) -> Result<String, MuxError> {
52 debug!(
53 "Creating pane in session: {} with split: {:?}",
54 session_id, split
55 );
56
57 let split_arg = match split {
58 SplitType::Horizontal => "-v",
59 SplitType::Vertical => "-h",
60 SplitType::None => {
61 return Err(MuxError::NotSupported(
62 "Split type None not supported".to_string(),
63 ))
64 }
65 };
66
67 let output = self
68 .tmux_command(&[
69 "split-window",
70 split_arg,
71 "-t",
72 session_id,
73 "-P",
74 "-F",
75 "#{pane_id}",
76 ])
77 .await?;
78
79 Ok(output.trim().to_string())
80 }
81
82 async fn send_keys(&self, pane_id: &str, keys: &str) -> Result<(), MuxError> {
83 debug!("Sending keys to pane {}: {}", pane_id, keys);
84
85 self.tmux_command(&["send-keys", "-t", pane_id, keys, "Enter"])
86 .await?;
87 Ok(())
88 }
89
90 async fn capture_pane(&self, pane_id: &str) -> Result<String, MuxError> {
91 debug!("Capturing pane: {}", pane_id);
92
93 self.tmux_command(&["capture-pane", "-t", pane_id, "-p"])
94 .await
95 }
96
97 async fn list_sessions(&self) -> Result<Vec<Session>, MuxError> {
98 debug!("Listing tmux sessions");
99
100 let output = self.tmux_command(&[
101 "list-sessions",
102 "-F", "#{session_id}:#{session_name}:#{session_created}:#{session_windows}:#{session_attached}"
103 ]).await?;
104
105 let sessions = output
106 .lines()
107 .filter(|line| !line.is_empty())
108 .map(|line| {
109 let parts: Vec<&str> = line.split(':').collect();
110 if parts.len() >= 5 {
111 Session {
112 id: parts[0].trim_start_matches('$').to_string(),
113 name: parts[1].to_string(),
114 created_at: Utc::now(), window_count: parts[3].parse().unwrap_or(0),
116 attached: parts[4] == "1",
117 }
118 } else {
119 Session {
120 id: line.to_string(),
121 name: line.to_string(),
122 created_at: Utc::now(),
123 window_count: 0,
124 attached: false,
125 }
126 }
127 })
128 .collect();
129
130 Ok(sessions)
131 }
132
133 async fn kill_session(&self, session_id: &str) -> Result<(), MuxError> {
134 info!("Killing tmux session: {}", session_id);
135
136 self.tmux_command(&["kill-session", "-t", session_id])
137 .await?;
138 Ok(())
139 }
140
141 async fn kill_pane(&self, pane_id: &str) -> Result<(), MuxError> {
142 debug!("Killing pane: {}", pane_id);
143
144 self.tmux_command(&["kill-pane", "-t", pane_id]).await?;
145 Ok(())
146 }
147
148 async fn resize_pane(&self, pane_id: &str, size: PaneSize) -> Result<(), MuxError> {
149 debug!(
150 "Resizing pane {} to {}x{}",
151 pane_id, size.width, size.height
152 );
153
154 Err(MuxError::NotSupported(
156 "Absolute pane resizing not implemented for tmux".to_string(),
157 ))
158 }
159
160 async fn select_pane(&self, pane_id: &str) -> Result<(), MuxError> {
161 debug!("Selecting pane: {}", pane_id);
162
163 self.tmux_command(&["select-pane", "-t", pane_id]).await?;
164 Ok(())
165 }
166
167 async fn list_panes(&self, session_id: &str) -> Result<Vec<Pane>, MuxError> {
168 debug!("Listing panes for session: {}", session_id);
169
170 let output = self.tmux_command(&[
171 "list-panes",
172 "-t", session_id,
173 "-F", "#{pane_id}:#{pane_index}:#{pane_title}:#{pane_active}:#{pane_width}:#{pane_height}"
174 ]).await?;
175
176 let panes = output
177 .lines()
178 .filter(|line| !line.is_empty())
179 .map(|line| {
180 let parts: Vec<&str> = line.split(':').collect();
181 if parts.len() >= 6 {
182 Pane {
183 id: parts[0].to_string(),
184 session_id: session_id.to_string(),
185 index: parts[1].parse().unwrap_or(0),
186 title: parts[2].to_string(),
187 active: parts[3] == "1",
188 size: PaneSize {
189 width: parts[4].parse().unwrap_or(80),
190 height: parts[5].parse().unwrap_or(24),
191 },
192 }
193 } else {
194 Pane {
195 id: line.to_string(),
196 session_id: session_id.to_string(),
197 index: 0,
198 title: String::new(),
199 active: false,
200 size: PaneSize {
201 width: 80,
202 height: 24,
203 },
204 }
205 }
206 })
207 .collect();
208
209 Ok(panes)
210 }
211
212 async fn attach_session(&self, session_id: &str) -> Result<(), MuxError> {
213 info!("Attaching to tmux session: {}", session_id);
214
215 self.tmux_command(&["has-session", "-t", session_id])
218 .await?;
219 Ok(())
220 }
221
222 async fn detach_session(&self, session_id: &str) -> Result<(), MuxError> {
223 info!("Detaching from tmux session: {}", session_id);
224
225 self.tmux_command(&["detach-client", "-s", session_id])
226 .await?;
227 Ok(())
228 }
229}