use std::collections::HashMap; use std::os::unix::fs::FileTypeExt; use std::time::{Duration, SystemTime}; use std::{fs, io, process}; use suggest::Suggest; use zellij_utils::{ anyhow, consts::{ session_info_folder_for_session, session_layout_cache_file_name, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR, }, envs, humantime::format_duration, input::layout::Layout, interprocess::local_socket::LocalSocketStream, ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg}, }; pub(crate) fn get_sessions() -> Result, io::ErrorKind> { match fs::read_dir(&*ZELLIJ_SOCK_DIR) { Ok(files) => { let mut sessions = Vec::new(); files.for_each(|file| { let file = file.unwrap(); let file_name = file.file_name().into_string().unwrap(); let ctime = std::fs::metadata(&file.path()) .ok() .and_then(|f| f.created().ok()) .and_then(|d| d.elapsed().ok()) .unwrap_or_default(); let duration = Duration::from_secs(ctime.as_secs()); if file.file_type().unwrap().is_socket() && 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(crate) fn get_resurrectable_sessions() -> Vec<(String, Duration, Layout)> { 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()); let raw_layout = match std::fs::read_to_string(&layout_file_name) { Ok(raw_layout) => raw_layout, Err(_e) => { return None; }, }; let ctime = match std::fs::metadata(&layout_file_name) .and_then(|metadata| metadata.created()) { Ok(created) => Some(created), Err(_e) => None, }; let layout = match Layout::from_kdl( &raw_layout, layout_file_name.display().to_string(), None, None, ) { Ok(layout) => layout, Err(e) => { log::error!("Failed to parse resurrection layout file: {}", e); return None; }, }; 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())?; Some((session_name, elapsed_duration, layout)) }) .collect() }, Err(e) => { log::error!( "Failed to read session_info cache folder: \"{:?}\": {:?}", &*ZELLIJ_SESSION_INFO_CACHE_DIR, e ); vec![] }, } } pub(crate) 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 file.file_type()?.is_socket() && 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)), } } fn assert_socket(name: &str) -> bool { let path = &*ZELLIJ_SOCK_DIR.join(name); match LocalSocketStream::connect(path) { Ok(stream) => { let mut sender = IpcSenderWithContext::new(stream); let _ = sender.send(ClientToServerMsg::ConnStatus); let mut receiver: IpcReceiverWithContext = sender.get_receiver(); match receiver.recv() { Some((ServerToClientMsg::Connected, _)) => true, None | Some((_, _)) => false, } }, Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => { drop(fs::remove_file(path)); false }, Err(_) => false, } } pub(crate) 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(crate) 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(crate) enum ActiveSession { None, One(String), Many, } pub(crate) 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(crate) fn kill_session(name: &str) { let path = &*ZELLIJ_SOCK_DIR.join(name); match LocalSocketStream::connect(path) { Ok(stream) => { let _ = IpcSenderWithContext::new(stream).send(ClientToServerMsg::KillSession); }, Err(e) => { eprintln!("Error occurred: {:?}", e); process::exit(1); }, }; } pub(crate) fn delete_session(name: &str, force: bool) { if force { let path = &*ZELLIJ_SOCK_DIR.join(name); let _ = LocalSocketStream::connect(path).map(|stream| { IpcSenderWithContext::new(stream) .send(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(crate) 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, _layout)| (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(crate) 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(crate) 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(crate) fn resurrection_layout(session_name_to_resurrect: &str) -> Option { let resurrectable_sessions = get_resurrectable_sessions(); resurrectable_sessions .iter() .find_map(|(name, _timestamp, layout)| { if name == session_name_to_resurrect { Some(layout.clone()) } else { None } }) } pub(crate) 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(crate) 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(crate) fn assert_session_ne(name: &str) { if name.trim().is_empty() { eprintln!("Session name cannot be empty. Please provide a specific session name."); process::exit(1); } if name == "." || name == ".." { eprintln!("Invalid session name: \"{}\".", name); process::exit(1); } if name.contains('/') { eprintln!("Session name cannot contain '/'."); process::exit(1); } match session_exists(name) { Ok(result) if !result => { let resurrectable_sessions = get_resurrectable_sessions(); 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); } /// 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(crate) fn get_name_generator() -> impl Iterator { names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain) } 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", ];