1use std::io;
2use std::path::PathBuf;
3use std::process::Command;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct PaneInfo {
8 pub session: String,
9 pub window: u32,
10 pub pane: u32,
11 pub pane_pid: u32,
12 pub current_command: String,
13 pub current_path: PathBuf,
14}
15
16const SEP: char = '\u{1f}'; pub fn list_panes() -> io::Result<Vec<PaneInfo>> {
22 let fmt = format!(
23 "#{{session_name}}{0}#{{window_index}}{0}#{{pane_index}}{0}#{{pane_pid}}{0}#{{pane_current_command}}{0}#{{pane_current_path}}",
24 SEP,
25 );
26 let output = Command::new("tmux")
27 .args(["list-panes", "-a", "-F", &fmt])
28 .output()?;
29 if !output.status.success() {
30 return Err(io::Error::other(format!(
31 "tmux list-panes failed: {}",
32 String::from_utf8_lossy(&output.stderr).trim()
33 )));
34 }
35 let stdout = String::from_utf8_lossy(&output.stdout);
36 Ok(parse_panes(&stdout))
37}
38
39pub fn parse_panes(stdout: &str) -> Vec<PaneInfo> {
40 stdout
41 .lines()
42 .filter(|l| !l.is_empty())
43 .filter_map(parse_pane_line)
44 .collect()
45}
46
47fn parse_pane_line(line: &str) -> Option<PaneInfo> {
48 let mut parts = line.splitn(6, SEP);
49 let session = parts.next()?.to_string();
50 let window = parts.next()?.parse().ok()?;
51 let pane = parts.next()?.parse().ok()?;
52 let pane_pid = parts.next()?.parse().ok()?;
53 let current_command = parts.next()?.to_string();
54 let current_path = PathBuf::from(parts.next()?);
55 Some(PaneInfo {
56 session,
57 window,
58 pane,
59 pane_pid,
60 current_command,
61 current_path,
62 })
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 fn line(parts: &[&str]) -> String {
70 parts.join(&SEP.to_string())
71 }
72
73 #[test]
74 fn parse_panes_single_row() {
75 let input = line(&["main", "0", "0", "12345", "claude", "/home/me/dev/x"]);
76 let panes = parse_panes(&input);
77 assert_eq!(panes.len(), 1);
78 assert_eq!(panes[0].session, "main");
79 assert_eq!(panes[0].window, 0);
80 assert_eq!(panes[0].pane, 0);
81 assert_eq!(panes[0].pane_pid, 12345);
82 assert_eq!(panes[0].current_command, "claude");
83 assert_eq!(panes[0].current_path, PathBuf::from("/home/me/dev/x"));
84 }
85
86 #[test]
87 fn parse_panes_multiple_rows() {
88 let input = format!(
89 "{}\n{}\n",
90 line(&["a", "0", "0", "1", "zsh", "/tmp"]),
91 line(&["b", "1", "2", "9999", "claude", "/x/y"]),
92 );
93 let panes = parse_panes(&input);
94 assert_eq!(panes.len(), 2);
95 assert_eq!(panes[1].session, "b");
96 assert_eq!(panes[1].window, 1);
97 assert_eq!(panes[1].pane, 2);
98 assert_eq!(panes[1].pane_pid, 9999);
99 }
100
101 #[test]
102 fn parse_panes_skips_empty_and_malformed() {
103 let input = format!(
104 "\n{}\nnot-a-row\n{}\n",
105 line(&["a", "0", "0", "1", "zsh", "/tmp"]),
106 line(&["bad", "x", "0", "1", "zsh", "/tmp"]), );
108 let panes = parse_panes(&input);
109 assert_eq!(panes.len(), 1);
110 assert_eq!(panes[0].session, "a");
111 }
112
113 #[test]
114 fn parse_panes_tolerates_spaces_in_paths() {
115 let input = line(&["a", "0", "0", "1", "claude", "/path with/spaces and stuff"]);
116 let panes = parse_panes(&input);
117 assert_eq!(panes.len(), 1);
118 assert_eq!(
119 panes[0].current_path,
120 PathBuf::from("/path with/spaces and stuff")
121 );
122 }
123}