use std::os::unix::fs::FileTypeExt; use std::time::SystemTime; use std::{fs, io, process}; use suggest::Suggest; use zellij_utils::{ anyhow, consts::ZELLIJ_SOCK_DIR, envs, 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(); if file.file_type().unwrap().is_socket() && assert_socket(&file_name) { sessions.push(file_name); } }); Ok(sessions) }, Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.kind()), Err(_) => Ok(Vec::with_capacity(0)), } } 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(sessions: Vec) { let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into()); sessions.iter().for_each(|session| { let suffix = if curr_session == *session { " (current)" } else { "" }; println!("{}{}", session, 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()), 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 list_sessions() { let exit_code = match get_sessions() { Ok(sessions) if !sessions.is_empty() => { print_sessions(sessions); 0 }, Ok(_) => { eprintln!("No active zellij sessions found."); 1 }, 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.starts_with(prefix)).collect(); if filtered_sessions.iter().any(|s| *s == prefix) { return Ok(SessionNameMatch::Exact(prefix.to_string())); } Ok({ match &filtered_sessions[..] { [] => SessionNameMatch::None, [s] => SessionNameMatch::UniquePrefix(s.to_string()), _ => { SessionNameMatch::AmbiguousPrefix(filtered_sessions.into_iter().cloned().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), } } 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().suggest(name) { println!(" help: Did you mean `{}`?", sugg); } } }, 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 => 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", ];