use dialoguer::Confirm; use std::net::IpAddr; use std::{path::PathBuf, process, time::Duration}; #[cfg(feature = "web_server_capability")] use isahc::{config::RedirectPolicy, prelude::*, HttpClient, Request}; use zellij_client::{ os_input_output::get_client_os_input, start_client as start_client_impl, ClientInfo, }; use zellij_utils::sessions::{ assert_dead_session, assert_session, assert_session_ne, delete_session as delete_session_impl, generate_unique_session_name, get_active_session, 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, session_listing_error_message, validate_session_name, ActiveSession, SessionNameMatch, }; use zellij_utils::consts::session_layout_cache_file_name; #[cfg(feature = "web_server_capability")] use zellij_client::web_client::start_web_client as start_web_client_impl; #[cfg(feature = "web_server_capability")] use zellij_utils::web_server_commands::shutdown_all_webserver_instances; #[cfg(feature = "web_server_capability")] use zellij_utils::web_authentication_tokens::{ create_token, list_tokens, revoke_all_tokens, revoke_token, }; use miette::{Report, Result}; 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, }, setup::Setup, }; pub(crate) use zellij_utils::sessions::list_sessions; pub(crate) fn kill_all_sessions(yes: bool) { match get_sessions() { Ok(sessions) if sessions.is_empty() => { eprintln!("No active sl0ppij 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) { use std::collections::BTreeMap; use zellij_server::background_jobs::scan_session_list_default_dirs; let active_sessions: Vec = get_sessions() .unwrap_or_default() .iter() .map(|s| s.0.clone()) .collect(); let (_live_sessions, resurrectable_map) = scan_session_list_default_dirs(&String::new(), &[], &BTreeMap::new()); let mut resurrectable_sessions: Vec<(String, Duration)> = resurrectable_map.into_iter().collect(); if force { for entry in get_resurrectable_sessions() { if !resurrectable_sessions.iter().any(|(n, _)| *n == entry.0) { resurrectable_sessions.push(entry); } } } let dead_sessions: Vec<_> = if force { resurrectable_sessions } else { resurrectable_sessions .into_iter() .filter(|(name, _)| !active_sessions.contains(name)) .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) => { if let Err(e) = validate_session_name(target_session) { eprintln!("{}", e); process::exit(1); } 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); } #[cfg(feature = "web_server_capability")] pub(crate) fn start_web_server( opts: CliArgs, run_daemonized: bool, ip: Option, port: Option, cert: Option, key: Option, startup_timeout: Option, ) { // TODO: move this outside of this function 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); }, }; start_web_client_impl( config, config_options, opts.config, run_daemonized, ip, port, cert, key, startup_timeout, ); } #[cfg(not(feature = "web_server_capability"))] pub(crate) fn start_web_server( _opts: CliArgs, _run_daemonized: bool, _ip: Option, _port: Option, _cert: Option, _key: Option, _startup_timeout: Option, ) { log::error!( "This version of Zellij was compiled without web server support, cannot run web server!" ); eprintln!( "This version of Zellij was compiled without web server support, cannot run web server!" ); std::process::exit(2); } fn create_new_client() -> ClientInfo { ClientInfo::New(generate_unique_session_name_or_exit(), None, None) } #[cfg(feature = "web_server_capability")] pub(crate) fn stop_web_server() -> Result<(), String> { shutdown_all_webserver_instances().map_err(|e| e.to_string())?; Ok(()) } #[cfg(not(feature = "web_server_capability"))] pub(crate) fn stop_web_server() -> Result<(), String> { log::error!( "This version of Zellij was compiled without web server support, cannot stop web server!" ); eprintln!( "This version of Zellij was compiled without web server support, cannot stop web server!" ); std::process::exit(2); } #[cfg(feature = "web_server_capability")] pub(crate) fn create_auth_token(name: Option, read_only: bool) -> Result { // returns the token and it's name create_token(name, read_only) .map(|(token, token_name)| { let access_type = if read_only { " (read-only)" } else { "" }; format!("{}: {}{}", token_name, token, access_type) }) .map_err(|e| e.to_string()) } #[cfg(not(feature = "web_server_capability"))] pub(crate) fn create_auth_token(_name: Option, _read_only: bool) -> Result { log::error!( "This version of Zellij was compiled without web server support, cannot create auth token!" ); eprintln!( "This version of Zellij was compiled without web server support, cannot create auth token!" ); std::process::exit(2); } #[cfg(feature = "web_server_capability")] pub(crate) fn revoke_auth_token(token_name: &str) -> Result { revoke_token(token_name).map_err(|e| e.to_string()) } #[cfg(not(feature = "web_server_capability"))] pub(crate) fn revoke_auth_token(_token_name: &str) -> Result { log::error!( "This version of Zellij was compiled without web server support, cannot revoke auth token!" ); eprintln!( "This version of Zellij was compiled without web server support, cannot revoke auth token!" ); std::process::exit(2); } #[cfg(feature = "web_server_capability")] pub(crate) fn revoke_all_auth_tokens() -> Result { // returns the revoked count revoke_all_tokens().map_err(|e| e.to_string()) } #[cfg(not(feature = "web_server_capability"))] pub(crate) fn revoke_all_auth_tokens() -> Result { log::error!( "This version of Zellij was compiled without web server support, cannot revoke all tokens!" ); eprintln!( "This version of Zellij was compiled without web server support, cannot revoke all tokens!" ); std::process::exit(2); } #[cfg(feature = "web_server_capability")] pub(crate) fn list_auth_tokens() -> Result, String> { // returns the token list line by line list_tokens() .map(|tokens| { let mut res = vec![]; for t in tokens { let access_type = if t.read_only { " [READ-ONLY]" } else { "" }; res.push(format!( "{}: created at {}{}", t.name, t.created_at, access_type )) } res }) .map_err(|e| e.to_string()) } #[cfg(not(feature = "web_server_capability"))] pub(crate) fn list_auth_tokens() -> Result, String> { log::error!( "This version of Zellij was compiled without web server support, cannot list tokens!" ); eprintln!( "This version of Zellij was compiled without web server support, cannot list tokens!" ); std::process::exit(2); } /// Default timeout for web server status check (in seconds) pub const DEFAULT_WEB_SERVER_STATUS_TIMEOUT_SECS: u64 = 30; #[cfg(feature = "web_server_capability")] pub(crate) fn web_server_status( web_server_base_url: &str, timeout_secs: Option, ) -> Result { let timeout = Duration::from_secs(timeout_secs.unwrap_or(DEFAULT_WEB_SERVER_STATUS_TIMEOUT_SECS)); let http_client = HttpClient::builder() .timeout(timeout) .redirect_policy(RedirectPolicy::Follow) .build() .map_err(|e| e.to_string())?; let request = Request::get(format!("{}/info/version", web_server_base_url,)); let req = request.body(()).map_err(|e| e.to_string())?; let mut res = http_client.send(req).map_err(|e| e.to_string())?; let status_code = res.status(); if status_code == 200 { let body = res.bytes().map_err(|e| e.to_string())?; Ok(String::from_utf8_lossy(&body).to_string()) } else { Err(format!( "Failed to stop web server, got status code: {}", status_code )) } } #[cfg(not(feature = "web_server_capability"))] pub(crate) fn web_server_status( _web_server_base_url: &str, _timeout_secs: Option, ) -> Result { log::error!( "This version of Zellij was compiled without web server support, cannot get web server status!" ); eprintln!( "This version of Zellij was compiled without web server support, cannot get web server status!" ); std::process::exit(2); } 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 subscribe_to_session( subscribe_cli: zellij_utils::cli::SubscribeCli, requested_session_name: Option, _config: Option, ) { let session_name = match get_active_session() { ActiveSession::None => { eprintln!("There is no active session!"); std::process::exit(1); }, ActiveSession::One(session_name) => { if let Some(ref requested) = requested_session_name { if *requested != session_name { eprintln!( "Session '{}' not found. The following sessions are active:", requested ); eprintln!("{}", session_name); std::process::exit(1); } } session_name }, 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) { session_name } 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() { session_name } else { eprintln!("Please specify the session name to subscribe to. The following sessions are active:"); list_sessions(false, false, true); std::process::exit(1); } }, }; let os_input = get_os_input(zellij_client::os_input_output::get_cli_client_os_input); zellij_client::cli_client::start_subscribe_client( Box::new(os_input), &session_name, subscribe_cli, ); } 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) => { let exit_status = zellij_client::cli_client::start_cli_client( Box::new(os_input), session_name, actions, ); std::process::exit(exit_status); }, 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 sl0ppij 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 => match session_exists(session) { Ok(true) => ClientInfo::Attach(session_name.unwrap(), config_options), Ok(false) => ClientInfo::New(session_name.unwrap(), None, None), Err(kind) => { eprintln!("{}", session_listing_error_message(kind)); process::exit(1); }, }, Some(prefix) => match match_session_name(prefix) { Ok(SessionNameMatch::UniquePrefix(s)) | Ok(SessionNameMatch::Exact(s)) => { ClientInfo::Attach(s, config_options) }, Ok(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); }, Ok(SessionNameMatch::None) => { eprintln!("No session with the name '{}' found!", prefix); process::exit(1); }, Err(kind) => { eprintln!("{}", session_listing_error_message(kind)); process::exit(1); }, }, None => match get_active_session() { ActiveSession::None if create => create_new_client(), ActiveSession::None => { eprintln!("No active sl0ppij 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) { let ( config, client_layout_info, config_options, mut config_without_layout, mut 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 config_options = config_options.clone(); let mut opts = opts.clone(); let mut is_a_reconnect = false; let mut should_create_detached = false; let mut layout_info = client_layout_info.clone(); let mut new_session_cwd = None; 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 reload_config_from_disk( &mut config_without_layout, &mut config_options_without_layout, &opts, ); 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, token: None, remember: false, forget: false, ca_cert: None, insecure: false, })); } else { opts.command = None; opts.session = None; config_options.attach_to_session = None; } if let Some(reconnect_layout) = &reconnect_to_session.layout { layout_info = Some(reconnect_layout.clone()); } if let Some(cwd) = &reconnect_to_session.cwd { new_session_cwd = Some(cwd.clone()); } config = config_without_layout.clone(); config_options = config_options_without_layout.clone(); 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, token, remember, forget, ca_cert, insecure, })) = opts.command.clone() { if let Some(remote_session_url) = session_name.as_ref().and_then(|s| { if s.starts_with("http://") || s.starts_with("https://") { Some(s) } else { None } }) { if !cfg!(feature = "web_server_capability") { eprintln!("This version of Zellij was compiled without web/remote-attach capabilities."); std::process::exit(2); } if options.is_some() || create || create_background || force_run_commands { eprintln!("Cannot attach to remote session with options."); std::process::exit(2); } #[cfg(feature = "web_server_capability")] if let Err(e) = zellij_client::start_remote_client( Box::new(os_input.clone()), remote_session_url, token, remember, forget, ca_cert, insecure, config_options.client_async_worker_tasks, ) { eprintln!("{}", e); std::process::exit(2); } } else { 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 mut 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| match resurrection_layout(&s) { Ok(layout) => layout, Err(e) => { eprintln!("{}", e); process::exit(2); }, }); 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(), session_layout_cache_file_name(session_name.as_ref()), force_run_commands, new_session_cwd.clone(), ) }, _ => 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); } } if let Some(layout_info) = layout_info { client.set_layout_info(layout_info); } if let Some(new_session_cwd) = new_session_cwd { client.set_cwd(new_session_cwd); } 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, 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, layout_info, new_session_cwd), 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, ); reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options, client, 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(), layout_info, new_session_cwd), 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_or_exit(); start_client_plan(session_name.clone()); reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options, ClientInfo::New(session_name, layout_info, new_session_cwd), None, None, is_a_reconnect, should_create_detached, ); } } if reconnect_to_session.is_none() { break; } } } fn generate_unique_session_name_or_exit() -> String { let Some(unique_session_name) = generate_unique_session_name() else { eprintln!("Failed to generate a unique session name, giving up"); process::exit(1); }; unique_session_name } 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); } pub(crate) fn watch_session(session_name: Option, opts: CliArgs) { let (config, _, 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); }, }; // Resolve the session name to watch let client_info = match &session_name { Some(prefix) => match match_session_name(prefix).unwrap() { SessionNameMatch::UniquePrefix(s) | SessionNameMatch::Exact(s) => { ClientInfo::Watch(s, config_options.clone()) }, SessionNameMatch::AmbiguousPrefix(sessions) => { eprintln!( "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 => { eprintln!("No active sl0ppij sessions found."); process::exit(1); }, ActiveSession::One(name) => ClientInfo::Watch(name, config_options.clone()), ActiveSession::Many => { eprintln!("Please specify the session name to watch."); process::exit(1); }, }, }; let mut opts = opts.clone(); opts.session = Some(client_info.get_session_name().to_string()); let os_input = get_os_input(get_client_os_input); // Start the watcher client start_client_impl( Box::new(os_input), opts, config, config_options, client_info, None, // tab_position_to_focus None, // pane_id_to_focus false, // is_a_reconnect false, // should_create_detached ); } fn reload_config_from_disk( config_without_layout: &mut Config, config_options_without_layout: &mut Options, opts: &CliArgs, ) { match Setup::from_cli_args(&opts) { Ok((_, _, _, reloaded_config_without_layout, reloaded_config_options_without_layout)) => { *config_without_layout = reloaded_config_without_layout; *config_options_without_layout = reloaded_config_options_without_layout; }, Err(e) => { log::error!("Failed to reload config: {}", e); }, }; } pub fn get_config_options_from_cli_args(opts: &CliArgs) -> Result { Setup::from_cli_args(&opts) .map(|(_, _, config_options, _, _)| config_options) .map_err(|e| e.to_string()) }