use dialoguer::Confirm; use std::{fs::File, io::prelude::*, path::PathBuf, process, time::Duration}; use crate::sessions::{ assert_dead_session, assert_session, assert_session_ne, delete_session as delete_session_impl, get_active_session, get_name_generator, get_resurrectable_sessions, get_sessions, get_sessions_sorted_by_mtime, kill_session as kill_session_impl, match_session_name, print_sessions, print_sessions_with_index, resurrection_layout, 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, LayoutInfo}, envs, input::{ actions::Action, config::{Config, ConfigError}, layout::Layout, options::Options, }, miette::{Report, Result}, nix, setup::{find_default_config_dir, get_layout_dir, 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.0); } process::exit(0); }, Err(e) => { eprintln!("Error occurred: {:?}", e); process::exit(1); }, } } pub(crate) fn delete_all_sessions(yes: bool, force: bool) { let active_sessions: Vec = get_sessions() .unwrap_or_default() .iter() .map(|s| s.0.clone()) .collect(); let resurrectable_sessions = get_resurrectable_sessions(); let dead_sessions: Vec<_> = if force { resurrectable_sessions } else { resurrectable_sessions .iter() .filter(|(name, _, _)| !active_sessions.contains(name)) .cloned() .collect() }; if !yes { println!("WARNING: this action will delete all resurrectable sessions."); if !Confirm::new() .with_prompt("Do you want to continue?") .interact() .unwrap() { println!("Abort."); process::exit(1); } } for session in &dead_sessions { delete_session_impl(&session.0, force); } process::exit(0); } 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); }, } } pub(crate) fn delete_session(target_session: &Option, force: bool) { match target_session { Some(target_session) => { assert_dead_session(target_session, force); delete_session_impl(target_session, force); process::exit(0); }, None => { println!("Please specify the session name to delete."); 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); }, } } /// Client entrypoint for all [`zellij_utils::cli::CliAction`] /// /// Checks session to send the action to and attaches with client 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: Vec = get_sessions() .unwrap_or_default() .iter() .map(|s| s.0.clone()) .collect(); 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 ); list_sessions(false, false, true); 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:"); list_sessions(false, false, true); 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 .iter() .map(|s| (s.clone(), Duration::default(), false)) .collect(), false, false, true, ); 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:"); list_sessions(false, false, true); 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, config_without_layout, config_options_without_layout) = 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 mut config = config.clone(); let mut layout = layout.clone(); let mut config_options = config_options.clone(); let mut opts = opts.clone(); let mut is_a_reconnect = false; let mut should_create_detached = 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, create_background: false, force_run_commands: false, index: None, options: None, })); } else { opts.command = None; opts.session = None; config_options.attach_to_session = None; } if let Some(reconnect_layout) = &reconnect_to_session.layout { let layout_dir = config.options.layout_dir.clone().or_else(|| { get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir)) }); let new_session_layout = match reconnect_layout { LayoutInfo::BuiltIn(layout_name) => Layout::from_default_assets( &PathBuf::from(layout_name), layout_dir.clone(), config_without_layout.clone(), ), LayoutInfo::File(layout_name) => Layout::from_path_or_default( Some(&PathBuf::from(layout_name)), layout_dir.clone(), config_without_layout.clone(), ), LayoutInfo::Url(url) => Layout::from_url(&url, config_without_layout.clone()), }; match new_session_layout { Ok(new_session_layout) => { // here we make sure to override both the layout and the config, but we do // this with an instance of the config before it was merged with the // layout configuration of the previous iteration of the loop, since we do // not want it to mix with the config of this session let (new_layout, new_layout_config) = new_session_layout; layout = new_layout; if let Some(cwd) = reconnect_to_session.cwd.as_ref() { layout.add_cwd_to_layout(cwd); } let mut new_config = config_without_layout.clone(); let _ = new_config.merge(new_layout_config.clone()); config = new_config; config_options = config_options_without_layout.merge(new_layout_config.options); }, Err(e) => { log::error!("Failed to parse new session layout: {:?}", e); }, } } 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, create_background, force_run_commands, 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, }; should_create_detached = create_background; let client = if let Some(idx) = index { attach_with_session_index( config_options.clone(), idx, create || should_create_detached, ) } else { let session_exists = session_name .as_ref() .and_then(|s| session_exists(&s).ok()) .unwrap_or(false); let resurrection_layout = session_name.as_ref().and_then(|s| resurrection_layout(&s)); if (create || should_create_detached) && !session_exists && resurrection_layout.is_none() { session_name.clone().map(start_client_plan); } match (session_name.as_ref(), resurrection_layout) { (Some(session_name), Some(mut resurrection_layout)) if !session_exists => { if force_run_commands { resurrection_layout.recursively_add_start_suspended(Some(false)); } ClientInfo::Resurrect(session_name.clone(), resurrection_layout) }, _ => attach_with_session_name( session_name, config_options.clone(), create || should_create_detached, ), } }; 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), ClientInfo::Resurrect(_session_name, layout_to_resurrect) => { Some(layout_to_resurrect.clone()) }, }; 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, should_create_detached, ); } 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, should_create_detached, ); } 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), ClientInfo::Resurrect(_, resurrection_layout) => { Some(resurrection_layout.clone()) }, }; reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options, client, attach_layout, None, None, is_a_reconnect, should_create_detached, ); }, _ => { 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, should_create_detached, ); }, } 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, should_create_detached, ); } } if reconnect_to_session.is_none() { break; } } } fn generate_unique_session_name() -> String { let sessions = get_sessions().map(|sessions| { sessions .iter() .map(|s| s.0.clone()) .collect::>() }); let dead_sessions: Vec = get_resurrectable_sessions() .iter() .map(|(s, _, _)| s.clone()) .collect(); 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) && !dead_sessions.contains(name)); if let Some(name) = name { return name; } else { eprintln!("Failed to generate a unique session name, giving up"); process::exit(1); } } pub(crate) fn list_aliases(opts: CliArgs) { let (config, _layout, _config_options, _config_without_layout, _config_options_without_layout) = 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); }, }; for alias in config.plugins.list() { println!("{}", alias); } process::exit(0); }