use crate::{ consts::{ is_ipc_socket, session_info_folder_for_session, session_layout_cache_file_name, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR, }, envs, input::layout::Layout, ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg}, }; use anyhow; use humantime::format_duration; use std::collections::HashMap; use std::time::{Duration, SystemTime}; use std::{fs, io, process}; use suggest::Suggest; pub fn get_sessions() -> Result, io::ErrorKind> { match fs::read_dir(&*ZELLIJ_SOCK_DIR) { Ok(files) => { let mut sessions = Vec::new(); files.for_each(|file| { if let Ok(file) = file { let file_name = file.file_name().into_string().unwrap(); // try to get creation time, fall back to modification time on platforms where it's not supported (e.g., musl) // for session creation time these are almost always identical (notable // exceptions are session name changes) let ctime = std::fs::metadata(&file.path()) .ok() .and_then(|f| f.created().ok().or_else(|| f.modified().ok())) .and_then(|d| d.elapsed().ok()) .unwrap_or_default(); let duration = Duration::from_secs(ctime.as_secs()); if is_ipc_socket(&file.file_type().unwrap()) && assert_socket(&file_name) { sessions.push((file_name, duration)); } } }); Ok(sessions) }, Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.kind()), Err(_) => Ok(Vec::with_capacity(0)), } } pub fn get_resurrectable_sessions() -> Vec<(String, Duration)> { match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) { Ok(files_in_session_info_folder) => { let files_that_are_folders = files_in_session_info_folder .filter_map(|f| f.ok().map(|f| f.path())) .filter(|f| f.is_dir()); files_that_are_folders .filter_map(|folder_name| { let layout_file_name = session_layout_cache_file_name(&folder_name.display().to_string()); // Try to get creation time, fall back to modification time on platforms where it's not supported (e.g., musl) let ctime = std::fs::metadata(&layout_file_name) .ok() .and_then(|metadata| { metadata.created().ok().or_else(|| metadata.modified().ok()) }); let elapsed_duration = ctime .map(|ctime| { Duration::from_secs(ctime.elapsed().ok().unwrap_or_default().as_secs()) }) .unwrap_or_default(); let session_name = folder_name .file_name() .map(|f| std::path::PathBuf::from(f).display().to_string())?; if std::path::Path::new(&layout_file_name).exists() { Some((session_name, elapsed_duration)) } else { None } }) .collect() }, Err(e) => { log::error!( "Failed to read session_info cache folder: \"{:?}\": {:?}", &*ZELLIJ_SESSION_INFO_CACHE_DIR, e ); vec![] }, } } pub fn get_resurrectable_session_names() -> Vec { match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) { Ok(files_in_session_info_folder) => { let files_that_are_folders = files_in_session_info_folder .filter_map(|f| f.ok().map(|f| f.path())) .filter(|f| f.is_dir()); files_that_are_folders .filter_map(|folder_name| { let folder = folder_name.display().to_string(); let resurrection_layout_file = session_layout_cache_file_name(&folder); if std::path::Path::new(&resurrection_layout_file).exists() { folder_name .file_name() .map(|f| format!("{}", f.to_string_lossy())) } else { None } }) .collect() }, Err(e) => { log::error!( "Failed to read session_info cache folder: \"{:?}\": {:?}", &*ZELLIJ_SESSION_INFO_CACHE_DIR, e ); vec![] }, } } pub fn get_sessions_sorted_by_mtime() -> anyhow::Result> { match fs::read_dir(&*ZELLIJ_SOCK_DIR) { Ok(files) => { let mut sessions_with_mtime: Vec<(String, SystemTime)> = Vec::new(); for file in files { let file = file?; let file_name = file.file_name().into_string().unwrap(); let file_modified_at = file.metadata()?.modified()?; if is_ipc_socket(&file.file_type()?) && assert_socket(&file_name) { sessions_with_mtime.push((file_name, file_modified_at)); } } sessions_with_mtime.sort_by_key(|x| x.1); // the oldest one will be the first let sessions = sessions_with_mtime.iter().map(|x| x.0.clone()).collect(); Ok(sessions) }, Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.into()), Err(_) => Ok(Vec::with_capacity(0)), } } /// Probe a session socket to check if a server is alive. /// /// On Unix, connects and sends a `ConnStatus` message to verify the server responds. /// On Windows, reads the server PID from the marker file and checks process liveness. #[cfg(unix)] fn assert_socket(name: &str) -> bool { use crate::consts::ipc_connect; let path = &*ZELLIJ_SOCK_DIR.join(name); match ipc_connect(path) { Ok(stream) => { let mut sender: IpcSenderWithContext = IpcSenderWithContext::new(stream); let _ = sender.send_client_msg(ClientToServerMsg::ConnStatus); let mut receiver: IpcReceiverWithContext = sender.get_receiver(); match receiver.recv_server_msg() { Some((ServerToClientMsg::Connected, _)) => true, None | Some((_, _)) => false, } }, Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => { drop(fs::remove_file(path)); false }, Err(_) => false, } } /// On Windows, reads the server PID from the marker file and checks whether /// the process is still alive via `OpenProcess`. Cleans up stale marker files. #[cfg(windows)] fn assert_socket(name: &str) -> bool { use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION}; let path = &*ZELLIJ_SOCK_DIR.join(name); let pid_str = match fs::read_to_string(path) { Ok(s) => s, Err(_) => { drop(fs::remove_file(path)); return false; }, }; let pid: u32 = match pid_str.trim().parse() { Ok(p) => p, Err(_) => { // Marker file exists but has no valid PID (e.g. empty from old version). // Treat as stale. drop(fs::remove_file(path)); return false; }, }; let alive = unsafe { let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid); if handle.is_null() { false } else { CloseHandle(handle); true } }; if !alive { drop(fs::remove_file(path)); } alive } #[cfg(not(any(unix, windows)))] fn assert_socket(_name: &str) -> bool { true } pub fn print_sessions( mut sessions: Vec<(String, Duration, bool)>, no_formatting: bool, short: bool, reverse: bool, ) { // (session_name, timestamp, is_dead) let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into()); sessions.sort_by(|a, b| { if reverse { // sort by `Duration` ascending (newest would be first) a.1.cmp(&b.1) } else { b.1.cmp(&a.1) } }); sessions .iter() .for_each(|(session_name, timestamp, is_dead)| { if short { println!("{}", session_name); return; } if no_formatting { let suffix = if curr_session == *session_name { format!("(current)") } else if *is_dead { format!("(EXITED - attach to resurrect)") } else { String::new() }; let timestamp = format!("[Created {} ago]", format_duration(*timestamp)); println!("{} {} {}", session_name, timestamp, suffix); } else { let formatted_session_name = format!("\u{1b}[32;1m{}\u{1b}[m", session_name); let suffix = if curr_session == *session_name { format!("(current)") } else if *is_dead { format!("(\u{1b}[31;1mEXITED\u{1b}[m - attach to resurrect)") } else { String::new() }; let timestamp = format!( "[Created \u{1b}[35;1m{}\u{1b}[m ago]", format_duration(*timestamp) ); println!("{} {} {}", formatted_session_name, timestamp, suffix); } }) } pub fn print_sessions_with_index(sessions: Vec) { let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into()); for (i, session) in sessions.iter().enumerate() { let suffix = if curr_session == *session { " (current)" } else { "" }; println!("{}: {}{}", i, session, suffix); } } pub enum ActiveSession { None, One(String), Many, } pub fn get_active_session() -> ActiveSession { match get_sessions() { Ok(sessions) if sessions.is_empty() => ActiveSession::None, Ok(mut sessions) if sessions.len() == 1 => ActiveSession::One(sessions.pop().unwrap().0), Ok(_) => ActiveSession::Many, Err(e) => { eprintln!("Error occurred: {:?}", e); process::exit(1); }, } } pub fn kill_session(name: &str) { use crate::consts::ipc_connect; let path = &*ZELLIJ_SOCK_DIR.join(name); match ipc_connect(path) { Ok(stream) => { // On Windows, the server uses a dual-pipe architecture: the main pipe // for client→server and a reply pipe for server→client. We must: // 1. Connect to the reply pipe (so the server unblocks from // reply_listener.accept() and spawns the route thread) // 2. Send KillSession on the main pipe // 3. Wait for the Exit response on the reply pipe (so we don't // disconnect before the server processes the message) #[cfg(windows)] { let reply = crate::consts::ipc_connect_reply(path); let _ = IpcSenderWithContext::::new(stream) .send_client_msg(ClientToServerMsg::KillSession); if let Ok(reply_stream) = reply { let mut receiver: IpcReceiverWithContext = IpcReceiverWithContext::new(reply_stream); let _ = receiver.recv_server_msg(); } } #[cfg(not(windows))] { let _ = IpcSenderWithContext::::new(stream) .send_client_msg(ClientToServerMsg::KillSession); } }, Err(e) => { eprintln!("Error occurred: {:?}", e); process::exit(1); }, }; } pub fn delete_session(name: &str, force: bool) { if force { use crate::consts::ipc_connect; let path = &*ZELLIJ_SOCK_DIR.join(name); let _ = ipc_connect(path).ok().map(|stream| { #[cfg(windows)] { let reply = crate::consts::ipc_connect_reply(path); let _ = IpcSenderWithContext::::new(stream) .send_client_msg(ClientToServerMsg::KillSession); if let Ok(reply_stream) = reply { let mut receiver: IpcReceiverWithContext = IpcReceiverWithContext::new(reply_stream); let _ = receiver.recv_server_msg(); } } #[cfg(not(windows))] { IpcSenderWithContext::::new(stream) .send_client_msg(ClientToServerMsg::KillSession) .ok(); } }); } if let Err(e) = std::fs::remove_dir_all(session_info_folder_for_session(name)) { if e.kind() == std::io::ErrorKind::NotFound { eprintln!("Session: {:?} not found.", name); process::exit(2); } else { log::error!("Failed to remove session {:?}: {:?}", name, e); } } else { println!("Session: {:?} successfully deleted.", name); } } pub fn list_sessions(no_formatting: bool, short: bool, reverse: bool) { let exit_code = match get_sessions() { Ok(running_sessions) => { let resurrectable_sessions = get_resurrectable_sessions(); let mut all_sessions: HashMap = resurrectable_sessions .iter() .map(|(name, timestamp)| (name.clone(), (timestamp.clone(), true))) .collect(); for (session_name, duration) in running_sessions { all_sessions.insert(session_name.clone(), (duration, false)); } if all_sessions.is_empty() { eprintln!("No active zellij sessions found."); 1 } else { print_sessions( all_sessions .iter() .map(|(name, (timestamp, is_dead))| { (name.clone(), timestamp.clone(), *is_dead) }) .collect(), no_formatting, short, reverse, ); 0 } }, Err(e) => { eprintln!("Error occurred: {:?}", e); 1 }, }; process::exit(exit_code); } #[derive(Debug, Clone)] pub enum SessionNameMatch { AmbiguousPrefix(Vec), UniquePrefix(String), Exact(String), None, } pub fn match_session_name(prefix: &str) -> Result { let sessions = get_sessions()?; let filtered_sessions: Vec<_> = sessions .iter() .filter(|s| s.0.starts_with(prefix)) .collect(); if filtered_sessions.iter().any(|s| s.0 == prefix) { return Ok(SessionNameMatch::Exact(prefix.to_string())); } Ok({ match &filtered_sessions[..] { [] => SessionNameMatch::None, [s] => SessionNameMatch::UniquePrefix(s.0.to_string()), _ => SessionNameMatch::AmbiguousPrefix( filtered_sessions.into_iter().map(|s| s.0.clone()).collect(), ), } }) } pub fn session_exists(name: &str) -> Result { match match_session_name(name) { Ok(SessionNameMatch::Exact(_)) => Ok(true), Ok(_) => Ok(false), Err(e) => Err(e), } } // if the session is resurrecable, the returned layout is the one to be used to resurrect it pub fn resurrection_layout(session_name_to_resurrect: &str) -> Result, String> { let layout_file_name = session_layout_cache_file_name(&session_name_to_resurrect); let raw_layout = match std::fs::read_to_string(&layout_file_name) { Ok(raw_layout) => raw_layout, Err(_e) => { return Ok(None); }, }; match Layout::from_kdl( &raw_layout, Some(layout_file_name.display().to_string()), None, None, ) { Ok(layout) => Ok(Some(layout)), Err(e) => { log::error!( "Failed to parse resurrection layout file {}: {}", layout_file_name.display(), e ); return Err(format!( "Failed to parse resurrection layout file {}: {}.", layout_file_name.display(), e )); }, } } pub fn assert_session(name: &str) { match session_exists(name) { Ok(result) => { if result { return; } else { println!("No session named {:?} found.", name); if let Some(sugg) = get_sessions() .unwrap() .iter() .map(|s| s.0.clone()) .collect::>() .suggest(name) { println!(" help: Did you mean `{}`?", sugg); } } }, Err(e) => { eprintln!("Error occurred: {:?}", e); }, }; process::exit(1); } pub fn assert_dead_session(name: &str, force: bool) { match session_exists(name) { Ok(exists) => { if exists && !force { println!( "A session by the name {:?} exists and is active, use --force to delete it.", name ) } else if exists && force { println!("A session by the name {:?} exists and is active, but will be force killed and deleted.", name); return; } else { return; } }, Err(e) => { eprintln!("Error occurred: {:?}", e); }, }; process::exit(1); } pub fn validate_session_name(name: &str) -> Result<(), String> { if name.trim().is_empty() { return Err( "Session name cannot be empty. Please provide a specific session name.".to_string(), ); } if name == "." || name == ".." { return Err(format!("Invalid session name: \"{}\".", name)); } if name.contains('/') { return Err("Session name cannot contain '/'.".to_string()); } Ok(()) } pub fn assert_session_ne(name: &str) { if let Err(e) = validate_session_name(name) { eprintln!("{}", e); process::exit(1); } match session_exists(name) { Ok(result) if !result => { let resurrectable_sessions = get_resurrectable_session_names(); if resurrectable_sessions.iter().find(|s| s == &name).is_some() { println!("Session with name {:?} already exists, but is dead. Use the attach command to resurrect it or, the delete-session command to kill it or specify a different name.", name); } else { return } } Ok(_) => println!("Session with name {:?} already exists. Use attach command to connect to it or specify a different name.", name), Err(e) => eprintln!("Error occurred: {:?}", e), }; process::exit(1); } pub fn generate_unique_session_name() -> Option { let sessions = get_sessions().map(|sessions| { sessions .iter() .map(|s| s.0.clone()) .collect::>() }); let dead_sessions = get_resurrectable_session_names(); let Ok(sessions) = sessions else { eprintln!("Failed to list existing sessions: {:?}", sessions); return None; }; let name = get_name_generator() .take(1000) .find(|name| !sessions.contains(name) && !dead_sessions.contains(name)); if let Some(name) = name { return Some(name); } else { return None; } } /// Create a new random name generator /// /// Used to provide a memorable handle for a session when users don't specify a session name when the session is /// created. /// /// Uses the list of adjectives and nouns defined below, with the intention of avoiding unfortunate /// and offensive combinations. Care should be taken when adding or removing to either list due to the birthday paradox/ /// hash collisions, e.g. with 4096 unique names, the likelihood of a collision in 10 session names is 1%. pub fn get_name_generator() -> impl Iterator { names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain) } /// Generates a random human-readable name using curated adjectives and nouns. /// Returns a single name in the format: AdjectiveNoun (e.g., "BraveRustacean") pub fn generate_random_name() -> String { get_name_generator().next().unwrap() } const ADJECTIVES: &[&'static str] = &[ "adamant", "adept", "adventurous", "arcadian", "auspicious", "awesome", "blossoming", "brave", "charming", "chatty", "circular", "considerate", "cubic", "curious", "delighted", "didactic", "diligent", "effulgent", "erudite", "excellent", "exquisite", "fabulous", "fascinating", "friendly", "glowing", "gracious", "gregarious", "hopeful", "implacable", "inventive", "joyous", "judicious", "jumping", "kind", "likable", "loyal", "lucky", "marvellous", "mellifluous", "nautical", "oblong", "outstanding", "polished", "polite", "profound", "quadratic", "quiet", "rectangular", "remarkable", "rusty", "sensible", "sincere", "sparkling", "splendid", "stellar", "tenacious", "tremendous", "triangular", "undulating", "unflappable", "unique", "verdant", "vitreous", "wise", "zippy", ]; const NOUNS: &[&'static str] = &[ "aardvark", "accordion", "apple", "apricot", "bee", "brachiosaur", "cactus", "capsicum", "clarinet", "cowbell", "crab", "cuckoo", "cymbal", "diplodocus", "donkey", "drum", "duck", "echidna", "elephant", "foxglove", "galaxy", "glockenspiel", "goose", "hill", "horse", "iguanadon", "jellyfish", "kangaroo", "lake", "lemon", "lemur", "magpie", "megalodon", "mountain", "mouse", "muskrat", "newt", "oboe", "ocelot", "orange", "panda", "peach", "pepper", "petunia", "pheasant", "piano", "pigeon", "platypus", "quasar", "rhinoceros", "river", "rustacean", "salamander", "sitar", "stegosaurus", "tambourine", "tiger", "tomato", "triceratops", "ukulele", "viola", "weasel", "xylophone", "yak", "zebra", ];