use dialoguer::Confirm; use std::{fs::File, io::prelude::*, path::PathBuf, process}; use crate::sessions::{ assert_session, assert_session_ne, get_active_session, get_name_generator, get_sessions, get_sessions_sorted_by_mtime, kill_session as kill_session_impl, match_session_name, print_sessions, print_sessions_with_index, session_exists, ActiveSession, SessionNameMatch, }; use zellij_client::{ old_config_converter::{ config_yaml_to_config_kdl, convert_old_yaml_files, layout_yaml_to_layout_kdl, }, os_input_output::get_client_os_input, start_client as start_client_impl, ClientInfo, }; use zellij_server::{os_input_output::get_server_os_input, start_server as start_server_impl}; use zellij_utils::{ cli::{CliArgs, Command, SessionCommand, Sessions}, data::ConnectToSession, envs, input::{ actions::Action, config::{Config, ConfigError}, options::Options, }, miette::{Report, Result}, nix, setup::Setup, }; pub(crate) use crate::sessions::list_sessions; pub(crate) fn kill_all_sessions(yes: bool) { match get_sessions() { Ok(sessions) if sessions.is_empty() => { eprintln!("No active zellij sessions found."); process::exit(1); }, Ok(sessions) => { if !yes { println!("WARNING: this action will kill all sessions."); if !Confirm::new() .with_prompt("Do you want to continue?") .interact() .unwrap() { println!("Abort."); process::exit(1); } } for session in &sessions { kill_session_impl(session); } process::exit(0); }, Err(e) => { eprintln!("Error occurred: {:?}", e); process::exit(1); }, } } pub(crate) fn kill_session(target_session: &Option) { match target_session { Some(target_session) => { assert_session(target_session); kill_session_impl(target_session); process::exit(0); }, None => { println!("Please specify the session name to kill."); process::exit(1); }, } } fn get_os_input( fn_get_os_input: fn() -> Result, ) -> OsInputOutput { match fn_get_os_input() { Ok(os_input) => os_input, Err(e) => { eprintln!("failed to open terminal:\n{}", e); process::exit(1); }, } } pub(crate) fn start_server(path: PathBuf, debug: bool) { // Set instance-wide debug mode zellij_utils::consts::DEBUG_MODE.set(debug).unwrap(); let os_input = get_os_input(get_server_os_input); start_server_impl(Box::new(os_input), path); } fn create_new_client() -> ClientInfo { ClientInfo::New(generate_unique_session_name()) } fn find_indexed_session( sessions: Vec, config_options: Options, index: usize, create: bool, ) -> ClientInfo { match sessions.get(index) { Some(session) => ClientInfo::Attach(session.clone(), config_options), None if create => create_new_client(), None => { println!( "No session indexed by {} found. The following sessions are active:", index ); print_sessions_with_index(sessions); process::exit(1); }, } } pub(crate) fn send_action_to_session( cli_action: zellij_utils::cli::CliAction, requested_session_name: Option, config: Option, ) { match get_active_session() { ActiveSession::None => { eprintln!("There is no active session!"); std::process::exit(1); }, ActiveSession::One(session_name) => { if let Some(requested_session_name) = requested_session_name { if requested_session_name != session_name { eprintln!( "Session '{}' not found. The following sessions are active:", requested_session_name ); eprintln!("{}", session_name); std::process::exit(1); } } attach_with_cli_client(cli_action, &session_name, config); }, ActiveSession::Many => { let existing_sessions = get_sessions().unwrap(); if let Some(session_name) = requested_session_name { if existing_sessions.contains(&session_name) { attach_with_cli_client(cli_action, &session_name, config); } else { eprintln!( "Session '{}' not found. The following sessions are active:", session_name ); print_sessions(existing_sessions); std::process::exit(1); } } else if let Ok(session_name) = envs::get_session_name() { attach_with_cli_client(cli_action, &session_name, config); } else { eprintln!("Please specify the session name to send actions to. The following sessions are active:"); print_sessions(existing_sessions); std::process::exit(1); } }, }; } pub(crate) fn convert_old_config_file(old_config_file: PathBuf) { match File::open(&old_config_file) { Ok(mut handle) => { let mut raw_config_file = String::new(); let _ = handle.read_to_string(&mut raw_config_file); match config_yaml_to_config_kdl(&raw_config_file, false) { Ok(kdl_config) => { println!("{}", kdl_config); process::exit(0); }, Err(e) => { eprintln!("Failed to convert config: {}", e); process::exit(1); }, } }, Err(e) => { eprintln!("Failed to open file: {}", e); process::exit(1); }, } } pub(crate) fn convert_old_layout_file(old_layout_file: PathBuf) { match File::open(&old_layout_file) { Ok(mut handle) => { let mut raw_layout_file = String::new(); let _ = handle.read_to_string(&mut raw_layout_file); match layout_yaml_to_layout_kdl(&raw_layout_file) { Ok(kdl_layout) => { println!("{}", kdl_layout); process::exit(0); }, Err(e) => { eprintln!("Failed to convert layout: {}", e); process::exit(1); }, } }, Err(e) => { eprintln!("Failed to open file: {}", e); process::exit(1); }, } } pub(crate) fn convert_old_theme_file(old_theme_file: PathBuf) { match File::open(&old_theme_file) { Ok(mut handle) => { let mut raw_config_file = String::new(); let _ = handle.read_to_string(&mut raw_config_file); match config_yaml_to_config_kdl(&raw_config_file, true) { Ok(kdl_config) => { println!("{}", kdl_config); process::exit(0); }, Err(e) => { eprintln!("Failed to convert config: {}", e); process::exit(1); }, } }, Err(e) => { eprintln!("Failed to open file: {}", e); process::exit(1); }, } } fn attach_with_cli_client( cli_action: zellij_utils::cli::CliAction, session_name: &str, config: Option, ) { let os_input = get_os_input(zellij_client::os_input_output::get_cli_client_os_input); let get_current_dir = || std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); match Action::actions_from_cli(cli_action, Box::new(get_current_dir), config) { Ok(actions) => { zellij_client::cli_client::start_cli_client(Box::new(os_input), session_name, actions); std::process::exit(0); }, Err(e) => { eprintln!("{}", e); log::error!("Error sending action: {}", e); std::process::exit(2); }, } } fn attach_with_session_index(config_options: Options, index: usize, create: bool) -> ClientInfo { // Ignore the session_name when `--index` is provided match get_sessions_sorted_by_mtime() { Ok(sessions) if sessions.is_empty() => { if create { create_new_client() } else { eprintln!("No active zellij sessions found."); process::exit(1); } }, Ok(sessions) => find_indexed_session(sessions, config_options, index, create), Err(e) => { eprintln!("Error occurred: {:?}", e); process::exit(1); }, } } fn attach_with_session_name( session_name: Option, config_options: Options, create: bool, ) -> ClientInfo { match &session_name { Some(session) if create => { if session_exists(session).unwrap() { ClientInfo::Attach(session_name.unwrap(), config_options) } else { ClientInfo::New(session_name.unwrap()) } }, Some(prefix) => match match_session_name(prefix).unwrap() { SessionNameMatch::UniquePrefix(s) | SessionNameMatch::Exact(s) => { ClientInfo::Attach(s, config_options) }, SessionNameMatch::AmbiguousPrefix(sessions) => { println!( "Ambiguous selection: multiple sessions names start with '{}':", prefix ); print_sessions(sessions); process::exit(1); }, SessionNameMatch::None => { eprintln!("No session with the name '{}' found!", prefix); process::exit(1); }, }, None => match get_active_session() { ActiveSession::None if create => create_new_client(), ActiveSession::None => { eprintln!("No active zellij sessions found."); process::exit(1); }, ActiveSession::One(session_name) => ClientInfo::Attach(session_name, config_options), ActiveSession::Many => { println!("Please specify the session to attach to, either by using the full name or a unique prefix.\nThe following sessions are active:"); print_sessions(get_sessions().unwrap()); process::exit(1); }, }, } } pub(crate) fn start_client(opts: CliArgs) { // look for old YAML config/layout/theme files and convert them to KDL convert_old_yaml_files(&opts); let (config, layout, config_options) = match Setup::from_cli_args(&opts) { Ok(results) => results, Err(e) => { if let ConfigError::KdlError(error) = e { let report: Report = error.into(); eprintln!("{:?}", report); } else { eprintln!("{}", e); } process::exit(1); }, }; let mut reconnect_to_session: Option = None; let os_input = get_os_input(get_client_os_input); loop { let os_input = os_input.clone(); let config = config.clone(); let layout = layout.clone(); let mut config_options = config_options.clone(); let mut opts = opts.clone(); let mut is_a_reconnect = false; if let Some(reconnect_to_session) = &reconnect_to_session { // this is integration code to make session reconnects work with this existing, // untested and pretty involved function // // ideally, we should write tests for this whole function and refctor it if reconnect_to_session.name.is_some() { opts.command = Some(Command::Sessions(Sessions::Attach { session_name: reconnect_to_session.name.clone(), create: true, index: None, options: None, })); } else { opts.command = None; opts.session = None; config_options.attach_to_session = None; } is_a_reconnect = true; } let start_client_plan = |session_name: std::string::String| { assert_session_ne(&session_name); }; if let Some(Command::Sessions(Sessions::Attach { session_name, create, index, options, })) = opts.command.clone() { let config_options = match options.as_deref() { Some(SessionCommand::Options(o)) => { config_options.merge_from_cli(o.to_owned().into()) }, None => config_options, }; let client = if let Some(idx) = index { attach_with_session_index(config_options.clone(), idx, create) } else { let session_exists = session_name .as_ref() .and_then(|s| session_exists(&s).ok()) .unwrap_or(false); if create && !session_exists { session_name.clone().map(start_client_plan); } attach_with_session_name(session_name, config_options.clone(), create) }; if let Ok(val) = std::env::var(envs::SESSION_NAME_ENV_KEY) { if val == *client.get_session_name() { panic!("You are trying to attach to the current session (\"{}\"). This is not supported.", val); } } let attach_layout = match client { ClientInfo::Attach(_, _) => None, ClientInfo::New(_) => Some(layout), }; let tab_position_to_focus = reconnect_to_session .as_ref() .and_then(|r| r.tab_position.clone()); let pane_id_to_focus = reconnect_to_session .as_ref() .and_then(|r| r.pane_id.clone()); reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options, client, attach_layout, tab_position_to_focus, pane_id_to_focus, is_a_reconnect, ); } else { if let Some(session_name) = opts.session.clone() { start_client_plan(session_name.clone()); reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options, ClientInfo::New(session_name), Some(layout), None, None, is_a_reconnect, ); } else { if let Some(session_name) = config_options.session_name.as_ref() { if let Ok(val) = envs::get_session_name() { // This prevents the same type of recursion as above, only that here we // don't get the command to "attach", but to start a new session instead. // This occurs for example when declaring the session name inside a layout // file and then, from within this session, trying to open a new zellij // session with the same layout. This causes an infinite recursion in the // `zellij_server::terminal_bytes::listen` task, flooding the server and // clients with infinite `Render` requests. if *session_name == val { eprintln!("You are trying to attach to the current session (\"{}\"). Zellij does not support nesting a session in itself.", session_name); process::exit(1); } } match config_options.attach_to_session { Some(true) => { let client = attach_with_session_name( Some(session_name.clone()), config_options.clone(), true, ); let attach_layout = match client { ClientInfo::Attach(_, _) => None, ClientInfo::New(_) => Some(layout), }; reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options, client, attach_layout, None, None, is_a_reconnect, ); }, _ => { start_client_plan(session_name.clone()); reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options.clone(), ClientInfo::New(session_name.clone()), Some(layout), None, None, is_a_reconnect, ); }, } if reconnect_to_session.is_some() { continue; } // after we detach, this happens and so we need to exit before the rest of the // function happens process::exit(0); } let session_name = generate_unique_session_name(); start_client_plan(session_name.clone()); reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options, ClientInfo::New(session_name), Some(layout), None, None, is_a_reconnect, ); } } if reconnect_to_session.is_none() { break; } } } fn generate_unique_session_name() -> String { let sessions = get_sessions(); let Ok(sessions) = sessions else { eprintln!("Failed to list existing sessions: {:?}", sessions); process::exit(1); }; let name = get_name_generator() .take(1000) .find(|name| !sessions.contains(name)); if let Some(name) = name { return name; } else { eprintln!("Failed to generate a unique session name, giving up"); process::exit(1); } }