* work * moar work * notes * work * separate to terminal and control channels * stdin working * serve html web client initial * serve static assets loaded with include_dir * merge * enable_web_server config parameter * compile time flag to disable web server capability * rustfmt * add license to all xterm.js assets * mouse working except copy/paste * helpful comment * web client improvements - move script to js file - add favicon - add nerd font - change title TODO: investigate if font license embedded in otf is sufficient * get mouse to work properly * kitty keyboard support initial * fix wrong type in preload link * wip axum websocket handlers - upgrade axum to v0.8.1, enable ws feature - begin setup of websocket handlers - tidy up imports * replace control listener * handle terminal websocket with axum * cleanup Cargo.toml * kitty fixes and bracketed paste * fix(mouse): pane not found crash * initial session switching infra * add `web_client_font` option * session switching, creation and resurrection working through the session manager * move session module to zellij-utils and share logic with web-client * some cleanups * require restart for enable-web-server * use session name from router * write config to disk and watch for config changes * rename session name to ipc path * add basic panic handler, make render_to_client exit on channel close * use while let instead of loop * handle websocket close * add mouse motions * make clipboard work * add weblink handling and webgl rendering * add todo * fix: use session name instead of patch on session switch * use "default" layout for new sessions * ui indication for session being shared * share this session ui * plugin assets * Fix process crash on mac with notify watcher. Use poll watcher instead of recommended as a workaround. * make url session switching and creation work * start welcome screen on root url * scaffold control messages, set font from config * set dimensions on session start * bring back session name from url * send bytes on terminal websocket instead of json - create web client os input and id before websocket connection * draft ui * work * refactor ui * remove otf font, remove margins to avoid scrollbar * version query endpoint for server status * web session info query endpoint * refactor: move stuff around * add web client info to session metadata * make tests pass * populate real data in session list * remove unnecessary endpoint * add web_client node to config, add font option * remove web_client_font * allow disabling the web session through the config - WIP * formalize sharing/not-sharing configuration * fix tests * allow shutting down web server * display error when web clients are forbidden to attach * only show sessions that allow web clients if this is a web client * style(fmt): rustfmt * fix: query web server from Zellij rather than from each plugin * remove log spam * handle some error paths better in the web client * allow controlling the web server through the cli * allow configuring the web server's ip/port * fix tests and format code * use direct WebServerStatus event instead of piggy-backing on SessionInfo * plugin revamp initial * make plugin responsive * adjust plugin title * refactor: share plugin * refactor: share plugin * add cors middleware * some fixes for running without a compiled web server capability * display error when starting the share plugin without web server support * clarify config * add pipelines to compile zellij without web support * display error when unable to start web server * only query web server when share plugin is running * refactor(web-client): connection table * give zellij_server_listener access to the control channel * fixes and clarifications * refactor: consolidate generate_unique_session_name * give proper error when trying to attach to a forbidden session * change browser URL when switching sessions * add keyboard shortcut * enforce https when bound to non-loopback ip * initial authentication token implementation * background color from theme * initial web client theme config * basic token generation ui * refactor set config message creation * also set body background * allow editing scrollback for plugins too * set scrollback to 0 * properly parse colors in config * generate token from plugin * nice login modals * initial token management screen * implement token authentication * refactor(share): token management screen * style(fmt): rustfmt * fix(plugin): some minor bugs * refactor(share): main screen * refactor(share): token screen * refactor(share): main * refactor(share): ui components * fix(responsiveness): properly send usage_width to the render function * fix cli commands and add some verbosity * add support for settings ansi and selection colors * add cursor and cursor accent * basic web client tests * fix tests * refactor: web client * use session tokens for authentication * improve modals * move shutdown to ipc * refactor: ipc logic * serialize theme config for web client * update tests * refactor: move some stuff around to prepare for config hot reload * config live reloading for the web clients * change remember-me UI wording * improve xterm.js link handling * make sure terminal is focused on mousemove * remove deprecated sharing indication from compact-bar * gate deps and functionality behind the web_server_compatibility feature * feat(build): add --no-web flag in all the places * fix some other build flows * add new assets * update CI for no-web (untested) * make more dependencies optional * update axum-extra * add web client configuration options * gracefully close connections on server exit * tests for graceful connection closing * handle client-side reconnect when server is down * fix: make sure ipc bus folder exists before starting * add commands to manage login tokens from the cli * style(fmt): rustfmt * some cleanups * fix(ux): allow alt-right-click on the web client without opening the context menu * fix: prevent attaching to welcome screen * fix: reload config issues * fix long socket path on macos * normalize config conversion and fix color gap in browser * revoke session_token cookie if it is not valid * fix: visual bug with multiple clients in extremely small screen sizes * fix: only include rusqlite for the web server capability builds * update e2e snapshots * refactor(web): client side js * some cleanups * moar cleanups * fix(tests): wait for server instead of using a fixed timeout * debug CI * fix(tests): use spawn_blocking for running the test web server * fix(tests): wait for http rather than tcp port * fix(tests): properly pass config path - hopefully this is the issue... * success! bring back the rest of the tests * attempt to fix the macos CI issue * docs(changelog): add PR --------- Co-authored-by: Thomas Linford <linford.t@gmail.com>
905 lines
33 KiB
Rust
905 lines
33 KiB
Rust
use dialoguer::Confirm;
|
|
use std::{fs::File, io::prelude::*, path::PathBuf, process, time::Duration};
|
|
|
|
#[cfg(feature = "web_server_capability")]
|
|
use isahc::{config::RedirectPolicy, prelude::*, HttpClient, Request};
|
|
|
|
use nix;
|
|
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_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, ActiveSession,
|
|
SessionNameMatch,
|
|
};
|
|
|
|
#[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, LayoutInfo},
|
|
envs,
|
|
input::{
|
|
actions::Action,
|
|
config::{Config, ConfigError},
|
|
layout::Layout,
|
|
options::Options,
|
|
},
|
|
setup::{find_default_config_dir, get_layout_dir, 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 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);
|
|
}
|
|
|
|
#[cfg(feature = "web_server_capability")]
|
|
pub(crate) fn start_web_server(opts: CliArgs, run_daemonized: bool) {
|
|
// 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);
|
|
}
|
|
|
|
#[cfg(not(feature = "web_server_capability"))]
|
|
pub(crate) fn start_web_server(_opts: CliArgs, _run_daemonized: bool) {
|
|
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())
|
|
}
|
|
|
|
#[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() -> Result<String, String> {
|
|
// returns the token and it's name
|
|
create_token(None)
|
|
.map(|(token_name, token)| format!("{}: {}", token, token_name))
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[cfg(not(feature = "web_server_capability"))]
|
|
pub(crate) fn create_auth_token() -> Result<String, String> {
|
|
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<bool, String> {
|
|
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<bool, String> {
|
|
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<usize, String> {
|
|
// 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<usize, String> {
|
|
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<Vec<String>, String> {
|
|
// returns the token list line by line
|
|
list_tokens()
|
|
.map(|tokens| {
|
|
let mut res = vec![];
|
|
for t in tokens {
|
|
res.push(format!("{}: created at {}", t.name, t.created_at))
|
|
}
|
|
res
|
|
})
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[cfg(not(feature = "web_server_capability"))]
|
|
pub(crate) fn list_auth_tokens() -> Result<Vec<String>, 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);
|
|
}
|
|
|
|
#[cfg(feature = "web_server_capability")]
|
|
pub(crate) fn web_server_status(web_server_base_url: &str) -> Result<String, String> {
|
|
let http_client = HttpClient::builder()
|
|
// TODO: 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) -> Result<String, String> {
|
|
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<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,
|
|
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<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
|
|
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,
|
|
}));
|
|
} 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);
|
|
},
|
|
}
|
|
} else {
|
|
if let Some(cwd) = reconnect_to_session.cwd.as_ref() {
|
|
config_options.default_cwd = Some(cwd.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,
|
|
})) = 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_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),
|
|
Some(layout),
|
|
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);
|
|
}
|
|
|
|
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<Options, String> {
|
|
Setup::from_cli_args(&opts)
|
|
.map(|(_, _, config_options, _, _)| config_options)
|
|
.map_err(|e| e.to_string())
|
|
}
|