Skip to main content

tmx/agents/
panes.rs

1use std::io;
2use std::path::PathBuf;
3use std::process::Command;
4
5/// One row from `tmux list-panes -a`.
6#[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
16/// Custom field separator. Pipe is the cleanest neutral char: not in shell
17/// commands, not in standard cwd paths, and not used by tmux's own format
18/// language for anything we touch here.
19const SEP: char = '\u{1f}'; // ASCII Unit Separator — guaranteed not in paths/cmds.
20
21pub 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"]), // window not a u32
107        );
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}