use super::{Focuser, MuxFocusResult};
use crate::util::{DEFAULT_TIMEOUT, run_command_with_timeout};
use std::collections::HashMap;
fn run_zellij_with_timeout(args: &[&str]) -> Option<std::process::Output> {
run_command_with_timeout("zellij", args, DEFAULT_TIMEOUT)
}
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_zellij_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;
}
}
if let Some(server_pid) = target_server {
if let Some(session_name) = find_session_for_server(server_pid, &sessions, &server_pids) {
return focus_session(&session_name, focuser);
}
if sessions.len() == 1 {
return focus_session(&sessions[0], focuser);
}
}
if sessions.len() == 1 {
return focus_session(&sessions[0], focuser);
}
for session in &sessions {
if let Some((client_tty, client_pid)) = get_session_client(session) {
let tty_device = client_tty.trim_start_matches("/dev/");
let _ = focuser.focus(tty_device, client_pid);
return MuxFocusResult::Switched {
session: session.clone(),
window: "1".to_string(),
pane: "0".to_string(),
};
}
}
MuxFocusResult::NotFound
}
fn list_sessions() -> Option<Vec<String>> {
let output = run_zellij_with_timeout(&["list-sessions", "-n"])?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let sessions: Vec<String> = stdout
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
line.split_whitespace().next().unwrap_or(line).to_string()
})
.collect();
Some(sessions)
}
fn find_zellij_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)
&& comm.contains("zellij-server")
{
servers.push(pid);
}
}
servers
}
fn find_session_for_server(
server_pid: i32,
sessions: &[String],
_server_pids: &[i32],
) -> Option<String> {
if sessions.len() == 1 {
return Some(sessions[0].clone());
}
if let Some(session) = find_session_from_server_sockets(server_pid, sessions) {
return Some(session);
}
for session in sessions {
if run_zellij_with_timeout(&["--session", session, "action", "list-clients"])
.is_some_and(|output| output.status.success())
{
return Some(session.clone());
}
}
None
}
fn find_session_from_server_sockets(server_pid: i32, sessions: &[String]) -> Option<String> {
let output = std::process::Command::new("lsof")
.args(["-p", &server_pid.to_string(), "-a", "-U", "-F", "n"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Some(path) = line.strip_prefix('n') {
if let Some(session_name) = path.rsplit('/').next() {
if sessions.iter().any(|s| s == session_name) {
return Some(session_name.to_string());
}
}
}
}
None
}
fn get_session_client(session: &str) -> Option<(String, Option<i32>)> {
let output = run_zellij_with_timeout(&["--session", session, "action", "list-clients"])?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let tty = parts[2];
let tty_normalized = if tty.starts_with("/dev/") {
tty.to_string()
} else {
format!("/dev/{}", tty)
};
for pid in prock::list_all_pids() {
if let Some(comm) = prock::get_process_comm(pid)
&& comm == "zellij"
&& let Some(proc_tty) = prock::get_tty(pid)
{
let proc_tty_normalized = if proc_tty.starts_with("/dev/") {
proc_tty
} else {
format!("/dev/{}", proc_tty)
};
if proc_tty_normalized == tty_normalized {
return Some((tty_normalized, Some(pid)));
}
}
}
return Some((tty_normalized, None));
}
}
None
}
fn focus_session(session: &str, focuser: &dyn Focuser) -> MuxFocusResult {
let _ = run_zellij_with_timeout(&["--session", session, "action", "go-to-tab", "1"]);
if let Some((client_tty, client_pid)) = get_session_client(session) {
let tty_device = client_tty.trim_start_matches("/dev/");
let _ = focuser.focus(tty_device, client_pid);
MuxFocusResult::Switched {
session: session.to_string(),
window: "1".to_string(),
pane: "0".to_string(),
}
} else {
MuxFocusResult::NoClient {
session: session.to_string(),
}
}
}
#[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_zellij_servers() {
let servers = find_zellij_servers();
assert!(servers.len() < 1000); }
#[test]
fn test_find_session_for_server_single_session() {
let sessions = vec!["my-session".to_string()];
let result = find_session_for_server(12345, &sessions, &[12345]);
assert_eq!(result, Some("my-session".to_string()));
}
#[test]
fn test_find_session_for_server_empty_sessions() {
let sessions: Vec<String> = vec![];
let result = find_session_for_server(12345, &sessions, &[12345]);
assert_eq!(result, None);
}
#[test]
fn test_parse_lsof_socket_path() {
let socket_path = "/tmp/zellij-501/my-session";
let session_name = socket_path.rsplit('/').next();
assert_eq!(session_name, Some("my-session"));
}
#[test]
fn test_parse_lsof_socket_path_with_subdirs() {
let socket_path = "/var/run/user/1000/zellij/session-name";
let session_name = socket_path.rsplit('/').next();
assert_eq!(session_name, Some("session-name"));
}
#[test]
fn test_tty_normalization() {
let tty_without_prefix = "ttys003";
let tty_with_prefix = "/dev/ttys003";
let normalized1 = if tty_without_prefix.starts_with("/dev/") {
tty_without_prefix.to_string()
} else {
format!("/dev/{}", tty_without_prefix)
};
let normalized2 = if tty_with_prefix.starts_with("/dev/") {
tty_with_prefix.to_string()
} else {
format!("/dev/{}", tty_with_prefix)
};
assert_eq!(normalized1, "/dev/ttys003");
assert_eq!(normalized2, "/dev/ttys003");
assert_eq!(normalized1, normalized2);
}
}