use super::{Focuser, MuxFocusResult};
use crate::util::{DEFAULT_TIMEOUT, run_command_with_timeout};
use std::collections::HashMap;
const MAX_PARENT_CHAIN_DEPTH: usize = 50;
fn run_screen_with_timeout(args: &[&str]) -> Option<std::process::Output> {
run_command_with_timeout("screen", args, DEFAULT_TIMEOUT)
}
fn find_window_number<S>(pid: i32, parent_map: &HashMap<i32, i32, S>) -> Option<String>
where
S: std::hash::BuildHasher,
{
let mut current_pid = pid;
for _ in 0..MAX_PARENT_CHAIN_DEPTH {
if let Some(environ) = prock::get_process_environ(current_pid)
&& let Some(window) = environ.get("WINDOW")
{
return Some(window.clone());
}
if let Some(&ppid) = parent_map.get(¤t_pid) {
if ppid <= 1 {
break;
}
current_pid = ppid;
} else {
break;
}
}
let target_tty = prock::get_tty(pid)?;
if target_tty == "?" {
return None;
}
for other_pid in prock::list_all_pids() {
if other_pid == pid {
continue;
}
if let Some(tty) = prock::get_tty(other_pid)
&& tty == target_tty
&& let Some(environ) = prock::get_process_environ(other_pid)
&& let Some(window) = environ.get("WINDOW")
{
return Some(window.clone());
}
}
None
}
fn switch_to_window(session: &str, window: &str) -> bool {
let result = run_screen_with_timeout(&["-S", session, "-p", window, "-X", "select", window]);
if result.as_ref().is_some_and(|o| o.status.success()) {
return true;
}
let keystroke = format!("\x01{}", window);
run_screen_with_timeout(&["-S", session, "-X", "stuff", &keystroke])
.is_some_and(|o| o.status.success())
}
pub fn focus_pane<S>(
pid: i32,
parent_map: &HashMap<i32, i32, S>,
focuser: &dyn Focuser,
) -> MuxFocusResult
where
S: std::hash::BuildHasher,
{
let sessions = match list_sessions() {
Some(s) if !s.is_empty() => s,
_ => return MuxFocusResult::NotFound,
};
let server_pids = find_screen_servers();
let mut target_server: Option<i32> = None;
for &server_pid in &server_pids {
if prock::is_descendant_of(pid, server_pid, parent_map) {
target_server = Some(server_pid);
break;
}
}
let window_number = find_window_number(pid, parent_map);
if let Some(server_pid) = target_server {
if let Some(session_name) = find_session_for_server(server_pid, &sessions) {
return focus_session_window(&session_name, window_number.as_deref(), focuser);
}
if sessions.len() == 1 {
return focus_session_window(&sessions[0].name, window_number.as_deref(), focuser);
}
}
if sessions.len() == 1 {
return focus_session_window(&sessions[0].name, window_number.as_deref(), focuser);
}
for session in &sessions {
if session.attached
&& let Some((client_tty, client_pid)) = get_session_client(&session.name)
{
if let Some(ref window) = window_number {
let _ = switch_to_window(&session.name, window);
}
let tty_device = client_tty.trim_start_matches("/dev/");
let _ = focuser.focus(tty_device, client_pid);
return MuxFocusResult::Switched {
session: session.name.clone(),
window: window_number.unwrap_or_else(|| "0".to_string()),
pane: "0".to_string(),
};
}
}
MuxFocusResult::NotFound
}
#[derive(Debug, Clone)]
struct ScreenSession {
name: String,
pid: i32,
attached: bool,
}
fn list_sessions() -> Option<Vec<ScreenSession>> {
let output = run_screen_with_timeout(&["-ls"])?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut sessions = Vec::new();
for line in stdout.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let Some(first_char) = trimmed.chars().next() else {
continue;
};
if !first_char.is_ascii_digit() {
continue;
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let name = parts[0];
let Some(pid_str) = name.split('.').next() else {
continue;
};
let Ok(pid) = pid_str.parse::<i32>() else {
continue;
};
let attached = parts.iter().any(|p| p.contains("Attached"));
sessions.push(ScreenSession {
name: name.to_string(),
pid,
attached,
});
}
Some(sessions)
}
fn find_screen_servers() -> Vec<i32> {
let all_pids = prock::list_all_pids();
let mut servers = Vec::new();
for pid in all_pids {
if let Some(comm) = prock::get_process_comm(pid) {
if comm == "screen" || comm == "SCREEN" {
servers.push(pid);
}
}
}
servers
}
fn find_session_for_server(server_pid: i32, sessions: &[ScreenSession]) -> Option<String> {
for session in sessions {
if session.pid == server_pid {
return Some(session.name.clone());
}
}
if sessions.len() == 1 {
return Some(sessions[0].name.clone());
}
None
}
fn get_session_client(session: &str) -> Option<(String, Option<i32>)> {
let session_pid: i32 = session.split('.').next()?.parse().ok()?;
let session_parts: Vec<&str> = session.split('.').collect();
let session_tty = session_parts.get(1).copied();
let parent_map = prock::build_parent_map();
let all_pids = prock::list_all_pids();
let mut fallback_candidate: Option<(String, i32)> = None;
for pid in all_pids {
if let Some(comm) = prock::get_process_comm(pid)
&& (comm == "screen" || comm == "SCREEN")
&& pid != session_pid
&& let Some(tty) = prock::get_tty(pid)
&& !tty.is_empty()
&& tty != "?"
{
let tty_path = if tty.starts_with("/dev/") {
tty.clone()
} else {
format!("/dev/{}", tty)
};
if let Some(s_tty) = session_tty
&& (tty == s_tty || tty.ends_with(s_tty))
{
return Some((tty_path, Some(pid)));
}
if prock::is_descendant_of(pid, session_pid, &parent_map)
|| parent_map.get(&pid) == Some(&session_pid)
{
return Some((tty_path, Some(pid)));
}
if fallback_candidate.is_none() {
fallback_candidate = Some((tty_path, pid));
}
}
}
fallback_candidate.map(|(tty, pid)| (tty, Some(pid)))
}
fn focus_session_window(
session: &str,
window: Option<&str>,
focuser: &dyn Focuser,
) -> MuxFocusResult {
if let Some((client_tty, client_pid)) = get_session_client(session) {
let tty_device = client_tty.trim_start_matches("/dev/");
let terminal = crate::detect::terminal(tty_device);
let _ = focuser.focus(tty_device, client_pid);
if let Some(window_num) = window {
let _ = switch_to_window(session, window_num);
if let Some(ref term) = terminal {
send_screen_keystroke_to_terminal(window_num, tty_device, term);
} else {
send_screen_keystroke(window_num);
}
}
MuxFocusResult::Switched {
session: session.to_string(),
window: window.unwrap_or("0").to_string(),
pane: "0".to_string(),
}
} else {
MuxFocusResult::NoClient {
session: session.to_string(),
}
}
}
#[cfg(target_os = "macos")]
fn send_screen_keystroke_to_terminal(window: &str, tty: &str, terminal: &crate::detect::Terminal) {
use crate::detect::TerminalKind;
if window.len() != 1 || !window.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return;
}
match &terminal.kind {
TerminalKind::ITerm2 => {
let script = format!(
r#"
tell application "iTerm2"
repeat with w in windows
repeat with t in tabs of w
repeat with s in sessions of t
if tty of s contains "{tty}" then
-- Send Ctrl-A (ASCII 1) followed by window number
tell s to write text (ASCII character 1) & "{window}" without newline
return true
end if
end repeat
end repeat
end repeat
return false
end tell
"#,
tty = tty,
window = window
);
let _ = std::process::Command::new("osascript")
.args(["-e", &script])
.output();
}
TerminalKind::TerminalApp => {
send_screen_keystroke(window);
}
_ => {
send_screen_keystroke(window);
}
}
}
#[cfg(not(target_os = "macos"))]
fn send_screen_keystroke_to_terminal(
_window: &str,
_tty: &str,
_terminal: &crate::detect::Terminal,
) {
}
#[cfg(target_os = "macos")]
fn send_screen_keystroke(window: &str) {
if window.len() != 1 || !window.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return;
}
let script = format!(
r#"
delay 0.1
tell application "System Events"
key code 0 using control down
delay 0.05
keystroke "{}"
end tell
"#,
window
);
let _ = std::process::Command::new("osascript")
.args(["-e", &script])
.output();
}
#[cfg(not(target_os = "macos"))]
fn send_screen_keystroke(_window: &str) {
}
#[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_nonexistent_pid() {
let parent_map = prock::build_parent_map();
let focuser = MockFocuser;
let result = focus_pane(999_999_999, &parent_map, &focuser);
match result {
MuxFocusResult::NotFound => {}
MuxFocusResult::Switched { .. } => {
}
MuxFocusResult::NoClient { .. } => {
}
MuxFocusResult::Error(_) => {}
}
}
#[test]
fn test_list_sessions() {
let sessions = list_sessions();
if let Some(s) = sessions {
assert!(s.len() < 1000); }
}
#[test]
fn test_find_screen_servers() {
let servers = find_screen_servers();
assert!(servers.len() < 1000); }
#[test]
fn test_find_session_for_server_single_session() {
let sessions = vec![ScreenSession {
name: "12345.my-session".to_string(),
pid: 12345,
attached: true,
}];
let result = find_session_for_server(12345, &sessions);
assert_eq!(result, Some("12345.my-session".to_string()));
}
#[test]
fn test_find_session_for_server_by_pid() {
let sessions = vec![
ScreenSession {
name: "12345.session1".to_string(),
pid: 12345,
attached: true,
},
ScreenSession {
name: "67890.session2".to_string(),
pid: 67890,
attached: false,
},
];
let result = find_session_for_server(67890, &sessions);
assert_eq!(result, Some("67890.session2".to_string()));
}
#[test]
fn test_find_session_for_server_not_found() {
let sessions = vec![
ScreenSession {
name: "12345.session1".to_string(),
pid: 12345,
attached: true,
},
ScreenSession {
name: "67890.session2".to_string(),
pid: 67890,
attached: false,
},
];
let result = find_session_for_server(99999, &sessions);
assert_eq!(result, None);
}
#[test]
fn test_parse_session_line() {
let line = "12345.pts-0.hostname\t(Attached)";
let parts: Vec<&str> = line.split_whitespace().collect();
assert!(!parts.is_empty());
let name = parts[0];
assert_eq!(name, "12345.pts-0.hostname");
let pid_str = name.split('.').next().unwrap();
let pid: i32 = pid_str.parse().unwrap();
assert_eq!(pid, 12345);
let attached = parts.iter().any(|p| p.contains("Attached"));
assert!(attached);
}
#[test]
fn test_parse_detached_session() {
let line = "54321.my-session\t(Detached)";
let parts: Vec<&str> = line.split_whitespace().collect();
let name = parts[0];
assert_eq!(name, "54321.my-session");
let attached = parts.iter().any(|p| p.contains("Attached"));
assert!(!attached);
let detached = parts.iter().any(|p| p.contains("Detached"));
assert!(detached);
}
#[test]
fn test_find_window_number_not_in_screen() {
let parent_map = prock::build_parent_map();
let our_pid = std::process::id() as i32;
let result = find_window_number(our_pid, &parent_map);
if let Some(window) = result {
assert!(
window.parse::<i32>().is_ok(),
"WINDOW should be a number, got: {}",
window
);
}
}
#[test]
fn test_find_window_number_invalid_pid() {
let parent_map = prock::build_parent_map();
let result = find_window_number(999_999_999, &parent_map);
assert!(result.is_none());
}
}