use std::path::{Path, PathBuf};
pub(crate) fn tmux_pane_sessions() -> Vec<(String, PathBuf)> {
let output = std::process::Command::new("tmux")
.args([
"list-panes",
"-a",
"-F",
"#{pane_current_path}\t#{session_name}",
])
.output();
match output {
Ok(o) if o.status.success() => parse_pane_sessions(&String::from_utf8_lossy(&o.stdout))
.into_iter()
.map(|(s, p)| {
let canon = p.canonicalize().unwrap_or(p);
(s, canon)
})
.collect(),
_ => Vec::new(),
}
}
fn parse_pane_sessions(output: &str) -> Vec<(String, PathBuf)> {
output
.lines()
.filter_map(|line| {
let (path, session) = line.split_once('\t')?;
let path = path.trim();
let session = session.trim();
if session.is_empty() || path.is_empty() {
return None;
}
Some((session.to_string(), PathBuf::from(path)))
})
.collect()
}
pub(crate) fn live_sessions(path: &Path, panes: &[(String, PathBuf)]) -> Vec<String> {
let mut names: Vec<String> = panes
.iter()
.filter(|(_, p)| p.starts_with(path))
.map(|(s, _)| s.clone())
.collect();
names.sort();
names.dedup();
names
}
pub(crate) fn is_live(path: &Path, panes: &[(String, PathBuf)]) -> bool {
panes.iter().any(|(_, p)| p.starts_with(path))
}
#[cfg(test)]
mod tests {
use super::*;
fn panes(ps: &[(&str, &str)]) -> Vec<(String, PathBuf)> {
ps.iter()
.map(|(s, p)| (s.to_string(), PathBuf::from(p)))
.collect()
}
#[test]
fn parse_skips_malformed_and_empty() {
let out = "/code/app\tmain\nno-tab\n/code/x\t\n\twork\n/code/app/src\tside\n";
assert_eq!(
parse_pane_sessions(out),
vec![
("main".to_string(), PathBuf::from("/code/app")),
("side".to_string(), PathBuf::from("/code/app/src")),
]
);
}
#[test]
fn parse_keeps_path_intact_when_session_name_has_tab() {
assert_eq!(
parse_pane_sessions("/code/app\tweird\tname\n"),
vec![("weird\tname".to_string(), PathBuf::from("/code/app"))]
);
}
#[test]
fn live_sessions_are_prefix_matched_unique_and_sorted() {
let p = panes(&[
("ternu", "/code/app/src"),
("fairtrail", "/code/app"),
("ternu", "/code/app"), ("other", "/code/app-sibling"), ]);
assert_eq!(
live_sessions(Path::new("/code/app"), &p),
vec!["fairtrail".to_string(), "ternu".to_string()]
);
}
#[test]
fn live_sessions_empty_when_none_inside() {
let p = panes(&[("x", "/elsewhere")]);
assert!(live_sessions(Path::new("/code/app"), &p).is_empty());
}
#[test]
fn is_live_matches_a_pane_inside_the_path() {
let p = panes(&[("s", "/code/app/src")]);
assert!(is_live(Path::new("/code/app"), &p));
assert!(!is_live(Path::new("/code/app-sibling"), &p));
assert!(!is_live(Path::new("/code/app"), &[]));
}
}