use super::{Focuser, MuxFocusResult};
use crate::util::{DEFAULT_TIMEOUT, run_command_with_timeout};
use std::process::Command;
use std::sync::mpsc;
use std::thread;
fn run_tmux_with_timeout(args: &[&str]) -> Option<std::process::Output> {
run_command_with_timeout("tmux", args, DEFAULT_TIMEOUT)
}
pub fn focus_pane<S>(
pid: i32,
parent_map: &std::collections::HashMap<i32, i32, S>,
focuser: &dyn Focuser,
) -> MuxFocusResult
where
S: std::hash::BuildHasher,
{
let output = match run_tmux_with_timeout(&[
"list-panes",
"-a",
"-F",
"#{pane_pid} #{session_name} #{window_index} #{pane_index}",
]) {
Some(o) => o,
None => return MuxFocusResult::NotFound,
};
if !output.status.success() {
return MuxFocusResult::NotFound;
}
let pane_list = String::from_utf8_lossy(&output.stdout);
for line in pane_list.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
continue;
}
let Ok(pane_pid) = parts[0].parse::<i32>() else {
continue;
};
let session_name = parts[1];
let window_index = parts[2];
let pane_index = parts[3];
if prock::is_descendant_of(pid, pane_pid, parent_map) {
let session_name_owned = session_name.to_string();
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let output = Command::new("tmux")
.args([
"list-clients",
"-t",
&session_name_owned,
"-F",
"#{client_tty} #{client_pid}",
])
.output();
let _ = tx.send(output);
});
let target = format!("{session_name}:{window_index}.{pane_index}");
let window_target = format!("{session_name}:{window_index}");
let pane_switched = run_tmux_with_timeout(&[
"select-window",
"-t",
&window_target,
";", "select-pane",
"-t",
&target,
])
.is_some_and(|o| o.status.success());
if pane_switched {
match rx.recv_timeout(DEFAULT_TIMEOUT) {
Ok(Ok(out)) if out.status.success() => {
let client_info = String::from_utf8_lossy(&out.stdout);
if let Some(line) = client_info.lines().next() {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(tty_path) = parts.first() {
let tty_device = tty_path.trim_start_matches("/dev/");
let client_pid = parts.get(1).and_then(|s| s.parse::<i32>().ok());
let _ = focuser.focus(tty_device, client_pid);
return MuxFocusResult::Switched {
session: session_name.to_string(),
window: window_index.to_string(),
pane: pane_index.to_string(),
};
}
}
return MuxFocusResult::NoClient {
session: session_name.to_string(),
};
}
Ok(Ok(_)) => {
return MuxFocusResult::Error("Failed to query tmux clients".to_string());
}
Ok(Err(_)) | Err(_) => {
return MuxFocusResult::Error("Timeout querying tmux clients".to_string());
}
}
}
return MuxFocusResult::Error("Failed to switch tmux pane".to_string());
}
}
MuxFocusResult::NotFound
}
#[cfg(test)]
mod tests {
use super::*;
struct MockFocuser;
impl Focuser for MockFocuser {
fn focus(&self, _tty: &str, _client_pid: Option<i32>) -> bool {
false
}
}
#[test]
fn test_focus_pane_tmux_not_available() {
let parent_map = prock::build_parent_map();
let focuser = MockFocuser;
let result = focus_pane(999_999_999, &parent_map, &focuser);
assert!(matches!(result, MuxFocusResult::NotFound));
}
}