zellij/src/commands.rs
Aram Drevekenin 4df7b42766
fix(startup): do not parse resurrectable sessions on startup (#3505)
* fix(startup): do not parse resurrectable sessions on startup

* style(fmt): rustfmt
2024-07-18 16:35:07 +02:00

713 lines
26 KiB
Rust

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_session_names,
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<String> = 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<String>) {
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<String>, 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<OsInputOutput>(
fn_get_os_input: fn() -> Result<OsInputOutput, nix::Error>,
) -> 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<String>,
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<String>,
config: Option<Config>,
) {
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<String> = 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<Config>,
) {
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<String>,
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<ConnectToSession> = 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()),
LayoutInfo::Stringified(stringified_layout) => Layout::from_stringified_layout(
&stringified_layout,
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::<Vec<String>>()
});
let dead_sessions = get_resurrectable_session_names();
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);
}