1use std::process::Command;
4
5pub struct WindowInfo {
8 pub index: u32,
9 pub name: String,
10 pub is_active: bool,
11 pub pane_path: String,
12}
13
14fn tmux(args: &[&str]) -> std::io::Result<std::process::Output> {
17 Command::new("tmux").args(args).output()
18}
19
20fn tmux_ok(args: &[&str]) -> bool {
21 tmux(args).is_ok_and(|o| o.status.success())
22}
23
24fn tmux_stdout(args: &[&str]) -> Result<String, String> {
25 let output = tmux(args).map_err(|e| format!("tmux: {e}"))?;
26 if !output.status.success() {
27 let stderr = String::from_utf8_lossy(&output.stderr);
28 return Err(format!("tmux: {}", stderr.trim()));
29 }
30 Ok(String::from_utf8_lossy(&output.stdout).to_string())
31}
32
33pub const SESSION: &str = "cove";
36
37pub fn has_session() -> bool {
38 tmux_ok(&["has-session", "-t", SESSION])
39}
40
41pub fn list_windows() -> Result<Vec<WindowInfo>, String> {
42 let out = tmux_stdout(&[
43 "list-windows",
44 "-t",
45 SESSION,
46 "-F",
47 "#{window_index}|#{window_name}|#{window_active}|#{pane_current_path}",
48 ])?;
49 Ok(parse_window_list(&out))
50}
51
52pub fn parse_window_list(output: &str) -> Vec<WindowInfo> {
55 let mut windows = Vec::new();
56 for line in output.lines() {
57 let parts: Vec<&str> = line.splitn(4, '|').collect();
58 if parts.len() < 4 {
59 continue;
60 }
61 windows.push(WindowInfo {
62 index: parts[0].parse().unwrap_or(0),
63 name: parts[1].to_string(),
64 is_active: parts[2] == "1",
65 pane_path: parts[3].to_string(),
66 });
67 }
68 windows
69}
70
71pub fn list_window_names() -> Result<Vec<String>, String> {
73 let out = tmux_stdout(&["list-windows", "-t", SESSION, "-F", "#{window_name}"])?;
74 Ok(out.lines().map(|s| s.to_string()).collect())
75}
76
77pub fn is_inside_tmux() -> bool {
78 std::env::var("TMUX").is_ok_and(|v| !v.is_empty())
79}
80
81pub fn new_session(name: &str, dir: &str, sidebar_bin: &str) -> Result<(), String> {
82 let status = Command::new("tmux")
83 .args([
84 "new-session",
85 "-d",
86 "-s",
87 SESSION,
88 "-n",
89 name,
90 "-c",
91 dir,
92 ";",
93 "set-option",
94 "-w",
95 "remain-on-exit",
96 "on",
97 ";",
98 "set-hook",
99 "pane-died",
100 "respawn-pane",
101 ";",
102 "split-window",
103 "-h",
104 "-p",
105 "30",
106 "-c",
107 dir,
108 ";",
109 "split-window",
110 "-t",
111 ".2",
112 "-v",
113 "-b",
114 "-p",
115 "50",
116 sidebar_bin,
117 ";",
118 "select-pane",
119 "-t",
120 ".2",
121 ";",
122 "respawn-pane",
123 "-t",
124 ".1",
125 "-k",
126 "claude",
127 ";",
128 "set-hook",
129 "-w",
130 "window-layout-changed",
131 "run-shell 'tmux resize-pane -t #{session_name}:#{window_index}.1 -x $((#{window_width} * 70 / 100))'",
132 ])
133 .status()
134 .map_err(|e| format!("tmux: {e}"))?;
135
136 if !status.success() {
137 return Err("tmux new-session failed".to_string());
138 }
139 Ok(())
140}
141
142pub fn new_window(name: &str, dir: &str) -> Result<(), String> {
143 let status = Command::new("tmux")
147 .args([
148 "new-window",
149 "-a",
150 "-t",
151 SESSION,
152 "-n",
153 name,
154 "-c",
155 dir,
156 "claude",
157 ])
158 .status()
159 .map_err(|e| format!("tmux: {e}"))?;
160
161 if !status.success() {
162 return Err("tmux new-window failed".to_string());
163 }
164 Ok(())
165}
166
167pub fn setup_layout(name: &str, dir: &str, sidebar_bin: &str) -> Result<(), String> {
168 let win = format!("{SESSION}:{name}");
169 let status = Command::new("tmux")
170 .args([
171 "set-option",
172 "-w",
173 "-t",
174 &win,
175 "remain-on-exit",
176 "on",
177 ";",
178 "set-hook",
179 "-w",
180 "-t",
181 &win,
182 "pane-died",
183 "respawn-pane",
184 ";",
185 "split-window",
186 "-t",
187 &win,
188 "-h",
189 "-p",
190 "30",
191 "-c",
192 dir,
193 ";",
194 "split-window",
195 "-t",
196 &format!("{win}.2"),
197 "-v",
198 "-b",
199 "-p",
200 "50",
201 sidebar_bin,
202 ";",
203 "select-pane",
204 "-t",
205 &format!("{win}.2"),
206 ";",
207 "set-hook",
208 "-w",
209 "-t",
210 &win,
211 "window-layout-changed",
212 &format!(
213 "run-shell 'tmux resize-pane -t {win}.1 -x $(( #{{window_width}} * 70 / 100 ))'"
214 ),
215 ])
216 .status()
217 .map_err(|e| format!("tmux: {e}"))?;
218
219 if !status.success() {
220 return Err("tmux setup-layout failed".to_string());
221 }
222 Ok(())
223}
224
225pub fn attach() -> Result<(), String> {
226 let status = Command::new("tmux")
227 .args(["attach", "-t", SESSION])
228 .status()
229 .map_err(|e| format!("tmux: {e}"))?;
230
231 if !status.success() {
232 return Err("tmux attach failed".to_string());
233 }
234 Ok(())
235}
236
237pub fn switch_client() -> Result<(), String> {
238 let status = Command::new("tmux")
239 .args(["switch-client", "-t", SESSION])
240 .status()
241 .map_err(|e| format!("tmux: {e}"))?;
242
243 if !status.success() {
244 return Err("tmux switch-client failed".to_string());
245 }
246 Ok(())
247}
248
249pub fn kill_window(name: &str) -> Result<(), String> {
250 let target = format!("{SESSION}:{name}");
251 tmux_stdout(&["kill-window", "-t", &target])?;
252 Ok(())
253}
254
255pub fn kill_session() -> Result<(), String> {
256 tmux_stdout(&["kill-session", "-t", SESSION])?;
257 Ok(())
258}
259
260pub fn select_window(index: u32) -> Result<(), String> {
261 let target = format!("{SESSION}:{index}");
262 let status = Command::new("tmux")
263 .args([
264 "select-window",
265 "-t",
266 &target,
267 ";",
268 "select-pane",
269 "-t",
270 ":.1",
271 ])
272 .status()
273 .map_err(|e| format!("tmux: {e}"))?;
274
275 if !status.success() {
276 return Err("tmux select-window failed".to_string());
277 }
278 Ok(())
279}
280
281pub struct PaneInfo {
283 pub window_index: u32,
284 pub command: String,
285 pub pane_id: String,
287}
288
289pub fn list_pane_commands() -> Result<Vec<PaneInfo>, String> {
291 let out = tmux_stdout(&[
292 "list-panes",
293 "-s",
294 "-t",
295 SESSION,
296 "-F",
297 "#{window_index}|#{pane_index}|#{pane_current_command}|#{pane_id}",
298 ])?;
299 Ok(parse_pane_list(&out))
300}
301
302pub fn parse_pane_list(output: &str) -> Vec<PaneInfo> {
306 let mut panes = Vec::new();
307 for line in output.lines() {
308 let parts: Vec<&str> = line.splitn(4, '|').collect();
309 if parts.len() < 4 {
310 continue;
311 }
312 if parts[1] != "1" {
314 continue;
315 }
316 panes.push(PaneInfo {
317 window_index: parts[0].parse().unwrap_or(0),
318 command: parts[2].to_string(),
319 pane_id: parts[3].to_string(),
320 });
321 }
322 panes
323}
324
325pub fn get_claude_pane_id(window_name: &str) -> Result<String, String> {
327 let target = format!("{SESSION}:{window_name}.1");
328 let out = tmux_stdout(&["display-message", "-t", &target, "-p", "#{pane_id}"])?;
329 Ok(out.trim().to_string())
330}
331
332pub fn set_window_option(window_name: &str, key: &str, value: &str) -> Result<(), String> {
333 let target = format!("{SESSION}:{window_name}");
334 tmux_stdout(&["set-option", "-w", "-t", &target, key, value])?;
335 Ok(())
336}
337
338pub fn get_window_option(pane_id: &str, key: &str) -> Result<String, String> {
339 let out = tmux_stdout(&["show-option", "-w", "-t", pane_id, "-v", key])?;
340 Ok(out.trim().to_string())
341}
342
343pub fn get_window_name(pane_id: &str) -> Result<String, String> {
344 let out = tmux_stdout(&["display-message", "-t", pane_id, "-p", "#{window_name}"])?;
345 Ok(out.trim().to_string())
346}
347
348pub fn rename_window(pane_id: &str, new_name: &str) -> Result<(), String> {
349 tmux_stdout(&["rename-window", "-t", pane_id, new_name])?;
350 Ok(())
351}
352
353pub fn send_keys(window_name: &str, keys: &[&str]) -> Result<(), String> {
355 let target = format!("{SESSION}:{window_name}.1");
356 let mut args = vec!["send-keys", "-t", &target];
357 args.extend_from_slice(keys);
358 tmux_stdout(&args)?;
359 Ok(())
360}
361
362pub fn pane_command(window_name: &str) -> Result<String, String> {
364 let target = format!("{SESSION}:{window_name}.1");
365 let out = tmux_stdout(&["display-message", "-t", &target, "-p", "#{pane_current_command}"])?;
366 Ok(out.trim().to_string())
367}
368
369pub fn disable_respawn(window_name: &str) -> Result<(), String> {
371 let target = format!("{SESSION}:{window_name}");
372 let _ = tmux_stdout(&["set-hook", "-u", "-w", "-t", &target, "pane-died"]);
373 let _ = tmux_stdout(&["set-option", "-w", "-t", &target, "remain-on-exit", "off"]);
374 Ok(())
375}
376
377pub fn select_window_sidebar(index: u32) -> Result<(), String> {
378 let target = format!("{SESSION}:{index}");
379 let status = Command::new("tmux")
380 .args([
381 "select-window",
382 "-t",
383 &target,
384 ";",
385 "select-pane",
386 "-t",
387 ":.2",
388 ])
389 .status()
390 .map_err(|e| format!("tmux: {e}"))?;
391
392 if !status.success() {
393 return Err("tmux select-window failed".to_string());
394 }
395 Ok(())
396}