diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-06-23 19:19:37 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-23 19:19:37 +0200 |
| commit | c5ac7968809ddb1033b625562eae22019cccc2f4 (patch) | |
| tree | 932cf07d898dfbcc6ec6a9d4cf5353c06a47accc /src | |
| parent | feat(ui): add optional tooltip with key hints to the compact bar (#4225) (diff) | |
| download | sl0ppij-c5ac7968809ddb1033b625562eae22019cccc2f4.tar.gz sl0ppij-c5ac7968809ddb1033b625562eae22019cccc2f4.tar.xz sl0ppij-c5ac7968809ddb1033b625562eae22019cccc2f4.zip | |
Feature: web-client/server to share your sessions in the browser (#4242)
* 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>
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands.rs | 227 | ||||
| -rw-r--r-- | src/main.rs | 93 | ||||
| -rw-r--r-- | src/sessions.rs | 600 |
3 files changed, 285 insertions, 635 deletions
diff --git a/src/commands.rs b/src/commands.rs index e01d5176..65134f08 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,15 +1,9 @@ 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 miette::{Report, Result}; +#[cfg(feature = "web_server_capability")] +use isahc::{config::RedirectPolicy, prelude::*, HttpClient, Request}; + use nix; use zellij_client::{ old_config_converter::{ @@ -18,6 +12,26 @@ 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, 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}, @@ -32,7 +46,7 @@ use zellij_utils::{ setup::{find_default_config_dir, get_layout_dir, Setup}, }; -pub(crate) use crate::sessions::list_sessions; +pub(crate) use zellij_utils::sessions::list_sessions; pub(crate) fn kill_all_sessions(yes: bool) { match get_sessions() { @@ -144,8 +158,166 @@ pub(crate) fn start_server(path: PathBuf, debug: bool) { 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()) + 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( @@ -660,7 +832,7 @@ pub(crate) fn start_client(opts: CliArgs) { process::exit(0); } - let session_name = generate_unique_session_name(); + 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), @@ -682,29 +854,12 @@ pub(crate) fn start_client(opts: CliArgs) { } } -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 { +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) { @@ -742,3 +897,9 @@ fn reload_config_from_disk( }, }; } + +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()) +} diff --git a/src/main.rs b/src/main.rs index 10459786..731774e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,16 @@ mod commands; -mod sessions; #[cfg(test)] mod tests; use clap::Parser; use zellij_utils::{ cli::{CliAction, CliArgs, Command, Sessions}, - consts::create_config_and_cache_folders, + consts::{create_config_and_cache_folders, VERSION}, envs, input::config::Config, logging::*, setup::Setup, + shared::web_server_base_url_from_config, }; fn main() { @@ -224,6 +224,95 @@ fn main() { opts.new_session_with_layout = None; opts.layout = Some(layout_for_new_session.clone()); commands::start_client(opts); + } else if let Some(Command::Web(web_opts)) = &opts.command { + if web_opts.get_start() { + let daemonize = web_opts.daemonize; + commands::start_web_server(opts, daemonize); + } else if web_opts.stop { + match commands::stop_web_server() { + Ok(()) => { + println!("Stopped web server."); + }, + Err(e) => { + eprintln!("Failed to stop web server: {}", e); + std::process::exit(2) + }, + } + } else if web_opts.status { + let config_options = commands::get_config_options_from_cli_args(&opts) + .expect("Can't find config options"); + let web_server_base_url = web_server_base_url_from_config(config_options); + match commands::web_server_status(&web_server_base_url) { + Ok(version) => { + let version = version.trim(); + println!( + "Web server online with version: {}. Checked: {}", + version, web_server_base_url + ); + if version != VERSION { + println!(""); + println!( + "Note: this version differs from the current Zellij version: {}.", + VERSION + ); + println!("Consider stopping the server with: zellij web --stop"); + println!("And then restarting it with: zellij web --start"); + } + }, + Err(_e) => { + println!("Web server is offline, checked: {}", web_server_base_url); + }, + } + } else if web_opts.create_token { + match commands::create_auth_token() { + Ok(token_and_name) => { + println!("Created token successfully"); + println!(""); + println!("{}", token_and_name); + }, + Err(e) => { + eprintln!("Failed to create token: {}", e); + std::process::exit(2) + }, + } + } else if let Some(token_name_to_revoke) = &web_opts.revoke_token { + match commands::revoke_auth_token(token_name_to_revoke) { + Ok(revoked) => { + if revoked { + println!("Successfully revoked token."); + } else { + eprintln!("Token by that name does not exist."); + std::process::exit(2) + } + }, + Err(e) => { + eprintln!("Failed to revoke token: {}", e); + std::process::exit(2) + }, + } + } else if web_opts.revoke_all_tokens { + match commands::revoke_all_auth_tokens() { + Ok(_) => { + println!("Successfully revoked all auth tokens"); + }, + Err(e) => { + eprintln!("Failed to revoke all auth tokens: {}", e); + std::process::exit(2) + }, + } + } else if web_opts.list_tokens { + match commands::list_auth_tokens() { + Ok(token_list) => { + for item in token_list { + println!("{}", item); + } + }, + Err(e) => { + eprintln!("Failed to list tokens: {}", e); + std::process::exit(2) + }, + } + } } else { commands::start_client(opts); } diff --git a/src/sessions.rs b/src/sessions.rs deleted file mode 100644 index 99c1036e..00000000 --- a/src/sessions.rs +++ /dev/null @@ -1,600 +0,0 @@ -use anyhow; -use humantime::format_duration; -use interprocess::local_socket::LocalSocketStream; -use std::collections::HashMap; -use std::os::unix::fs::FileTypeExt; -use std::time::{Duration, SystemTime}; -use std::{fs, io, process}; -use suggest::Suggest; -use zellij_utils::{ - consts::{ - session_info_folder_for_session, session_layout_cache_file_name, - ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR, - }, - envs, - input::layout::Layout, - ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg}, -}; - -pub(crate) fn get_sessions() -> Result<Vec<(String, Duration)>, io::ErrorKind> { - match fs::read_dir(&*ZELLIJ_SOCK_DIR) { - Ok(files) => { - let mut sessions = Vec::new(); - files.for_each(|file| { - let file = file.unwrap(); - let file_name = file.file_name().into_string().unwrap(); - let ctime = std::fs::metadata(&file.path()) - .ok() - .and_then(|f| f.created().ok()) - .and_then(|d| d.elapsed().ok()) - .unwrap_or_default(); - let duration = Duration::from_secs(ctime.as_secs()); - if file.file_type().unwrap().is_socket() && assert_socket(&file_name) { - sessions.push((file_name, duration)); - } - }); - Ok(sessions) - }, - Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.kind()), - Err(_) => Ok(Vec::with_capacity(0)), - } -} - -pub(crate) fn get_resurrectable_sessions() -> Vec<(String, Duration, Layout)> { - match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) { - Ok(files_in_session_info_folder) => { - let files_that_are_folders = files_in_session_info_folder - .filter_map(|f| f.ok().map(|f| f.path())) - .filter(|f| f.is_dir()); - files_that_are_folders - .filter_map(|folder_name| { - let layout_file_name = - session_layout_cache_file_name(&folder_name.display().to_string()); - let raw_layout = match std::fs::read_to_string(&layout_file_name) { - Ok(raw_layout) => raw_layout, - Err(_e) => { - return None; - }, - }; - let ctime = match std::fs::metadata(&layout_file_name) - .and_then(|metadata| metadata.created()) - { - Ok(created) => Some(created), - Err(_e) => None, - }; - let layout = match Layout::from_kdl( - &raw_layout, - Some(layout_file_name.display().to_string()), - None, - None, - ) { - Ok(layout) => layout, - Err(e) => { - log::error!("Failed to parse resurrection layout file: {}", e); - return None; - }, - }; - let elapsed_duration = ctime - .map(|ctime| { - Duration::from_secs(ctime.elapsed().ok().unwrap_or_default().as_secs()) - }) - .unwrap_or_default(); - let session_name = folder_name - .file_name() - .map(|f| std::path::PathBuf::from(f).display().to_string())?; - Some((session_name, elapsed_duration, layout)) - }) - .collect() - }, - Err(e) => { - log::error!( - "Failed to read session_info cache folder: \"{:?}\": {:?}", - &*ZELLIJ_SESSION_INFO_CACHE_DIR, - e - ); - vec![] - }, - } -} - -pub(crate) fn get_resurrectable_session_names() -> Vec<String> { - match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) { - Ok(files_in_session_info_folder) => { - let files_that_are_folders = files_in_session_info_folder - .filter_map(|f| f.ok().map(|f| f.path())) - .filter(|f| f.is_dir()); - files_that_are_folders - .filter_map(|folder_name| { - let folder = folder_name.display().to_string(); - let resurrection_layout_file = session_layout_cache_file_name(&folder); - if std::path::Path::new(&resurrection_layout_file).exists() { - folder_name - .file_name() - .map(|f| format!("{}", f.to_string_lossy())) - } else { - None - } - }) - .collect() - }, - Err(e) => { - log::error!( - "Failed to read session_info cache folder: \"{:?}\": {:?}", - &*ZELLIJ_SESSION_INFO_CACHE_DIR, - e - ); - vec![] - }, - } -} - -pub(crate) fn get_sessions_sorted_by_mtime() -> anyhow::Result<Vec<String>> { - match fs::read_dir(&*ZELLIJ_SOCK_DIR) { - Ok(files) => { - let mut sessions_with_mtime: Vec<(String, SystemTime)> = Vec::new(); - for file in files { - let file = file?; - let file_name = file.file_name().into_string().unwrap(); - let file_modified_at = file.metadata()?.modified()?; - if file.file_type()?.is_socket() && assert_socket(&file_name) { - sessions_with_mtime.push((file_name, file_modified_at)); - } - } - sessions_with_mtime.sort_by_key(|x| x.1); // the oldest one will be the first - - let sessions = sessions_with_mtime.iter().map(|x| x.0.clone()).collect(); - Ok(sessions) - }, - Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.into()), - Err(_) => Ok(Vec::with_capacity(0)), - } -} - -fn assert_socket(name: &str) -> bool { - let path = &*ZELLIJ_SOCK_DIR.join(name); - match LocalSocketStream::connect(path) { - Ok(stream) => { - let mut sender = IpcSenderWithContext::new(stream); - let _ = sender.send(ClientToServerMsg::ConnStatus); - let mut receiver: IpcReceiverWithContext<ServerToClientMsg> = sender.get_receiver(); - match receiver.recv() { - Some((ServerToClientMsg::Connected, _)) => true, - None | Some((_, _)) => false, - } - }, - Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => { - drop(fs::remove_file(path)); - false - }, - Err(_) => false, - } -} - -pub(crate) fn print_sessions( - mut sessions: Vec<(String, Duration, bool)>, - no_formatting: bool, - short: bool, - reverse: bool, -) { - // (session_name, timestamp, is_dead) - let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into()); - sessions.sort_by(|a, b| { - if reverse { - // sort by `Duration` ascending (newest would be first) - a.1.cmp(&b.1) - } else { - b.1.cmp(&a.1) - } - }); - sessions - .iter() - .for_each(|(session_name, timestamp, is_dead)| { - if short { - println!("{}", session_name); - return; - } - if no_formatting { - let suffix = if curr_session == *session_name { - format!("(current)") - } else if *is_dead { - format!("(EXITED - attach to resurrect)") - } else { - String::new() - }; - let timestamp = format!("[Created {} ago]", format_duration(*timestamp)); - println!("{} {} {}", session_name, timestamp, suffix); - } else { - let formatted_session_name = format!("\u{1b}[32;1m{}\u{1b}[m", session_name); - let suffix = if curr_session == *session_name { - format!("(current)") - } else if *is_dead { - format!("(\u{1b}[31;1mEXITED\u{1b}[m - attach to resurrect)") - } else { - String::new() - }; - let timestamp = format!( - "[Created \u{1b}[35;1m{}\u{1b}[m ago]", - format_duration(*timestamp) - ); - println!("{} {} {}", formatted_session_name, timestamp, suffix); - } - }) -} - -pub(crate) fn print_sessions_with_index(sessions: Vec<String>) { - let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into()); - for (i, session) in sessions.iter().enumerate() { - let suffix = if curr_session == *session { - " (current)" - } else { - "" - }; - println!("{}: {}{}", i, session, suffix); - } -} - -pub(crate) enum ActiveSession { - None, - One(String), - Many, -} - -pub(crate) fn get_active_session() -> ActiveSession { - match get_sessions() { - Ok(sessions) if sessions.is_empty() => ActiveSession::None, - Ok(mut sessions) if sessions.len() == 1 => ActiveSession::One(sessions.pop().unwrap().0), - Ok(_) => ActiveSession::Many, - Err(e) => { - eprintln!("Error occurred: {:?}", e); - process::exit(1); - }, - } -} - -pub(crate) fn kill_session(name: &str) { - let path = &*ZELLIJ_SOCK_DIR.join(name); - match LocalSocketStream::connect(path) { - Ok(stream) => { - let _ = IpcSenderWithContext::new(stream).send(ClientToServerMsg::KillSession); - }, - Err(e) => { - eprintln!("Error occurred: {:?}", e); - process::exit(1); - }, - }; -} - -pub(crate) fn delete_session(name: &str, force: bool) { - if force { - let path = &*ZELLIJ_SOCK_DIR.join(name); - let _ = LocalSocketStream::connect(path).map(|stream| { - IpcSenderWithContext::new(stream) - .send(ClientToServerMsg::KillSession) - .ok(); - }); - } - if let Err(e) = std::fs::remove_dir_all(session_info_folder_for_session(name)) { - if e.kind() == std::io::ErrorKind::NotFound { - eprintln!("Session: {:?} not found.", name); - process::exit(2); - } else { - log::error!("Failed to remove session {:?}: {:?}", name, e); - } - } else { - println!("Session: {:?} successfully deleted.", name); - } -} - -pub(crate) fn list_sessions(no_formatting: bool, short: bool, reverse: bool) { - let exit_code = match get_sessions() { - Ok(running_sessions) => { - let resurrectable_sessions = get_resurrectable_sessions(); - let mut all_sessions: HashMap<String, (Duration, bool)> = resurrectable_sessions - .iter() - .map(|(name, timestamp, _layout)| (name.clone(), (timestamp.clone(), true))) - .collect(); - for (session_name, duration) in running_sessions { - all_sessions.insert(session_name.clone(), (duration, false)); - } - if all_sessions.is_empty() { - eprintln!("No active zellij sessions found."); - 1 - } else { - print_sessions( - all_sessions - .iter() - .map(|(name, (timestamp, is_dead))| { - (name.clone(), timestamp.clone(), *is_dead) - }) - .collect(), - no_formatting, - short, - reverse, - ); - 0 - } - }, - Err(e) => { - eprintln!("Error occurred: {:?}", e); - 1 - }, - }; - process::exit(exit_code); -} - -#[derive(Debug, Clone)] -pub enum SessionNameMatch { - AmbiguousPrefix(Vec<String>), - UniquePrefix(String), - Exact(String), - None, -} - -pub(crate) fn match_session_name(prefix: &str) -> Result<SessionNameMatch, io::ErrorKind> { - let sessions = get_sessions()?; - - let filtered_sessions: Vec<_> = sessions - .iter() - .filter(|s| s.0.starts_with(prefix)) - .collect(); - - if filtered_sessions.iter().any(|s| s.0 == prefix) { - return Ok(SessionNameMatch::Exact(prefix.to_string())); - } - - Ok({ - match &filtered_sessions[..] { - [] => SessionNameMatch::None, - [s] => SessionNameMatch::UniquePrefix(s.0.to_string()), - _ => SessionNameMatch::AmbiguousPrefix( - filtered_sessions.into_iter().map(|s| s.0.clone()).collect(), - ), - } - }) -} - -pub(crate) fn session_exists(name: &str) -> Result<bool, io::ErrorKind> { - match match_session_name(name) { - Ok(SessionNameMatch::Exact(_)) => Ok(true), - Ok(_) => Ok(false), - Err(e) => Err(e), - } -} - -// if the session is resurrecable, the returned layout is the one to be used to resurrect it -pub(crate) fn resurrection_layout(session_name_to_resurrect: &str) -> Option<Layout> { - let resurrectable_sessions = get_resurrectable_sessions(); - resurrectable_sessions - .iter() - .find_map(|(name, _timestamp, layout)| { - if name == session_name_to_resurrect { - Some(layout.clone()) - } else { - None - } - }) -} - -pub(crate) fn assert_session(name: &str) { - match session_exists(name) { - Ok(result) => { - if result { - return; - } else { - println!("No session named {:?} found.", name); - if let Some(sugg) = get_sessions() - .unwrap() - .iter() - .map(|s| s.0.clone()) - .collect::<Vec<_>>() - .suggest(name) - { - println!(" help: Did you mean `{}`?", sugg); - } - } - }, - Err(e) => { - eprintln!("Error occurred: {:?}", e); - }, - }; - process::exit(1); -} - -pub(crate) fn assert_dead_session(name: &str, force: bool) { - match session_exists(name) { - Ok(exists) => { - if exists && !force { - println!( - "A session by the name {:?} exists and is active, use --force to delete it.", - name - ) - } else if exists && force { - println!("A session by the name {:?} exists and is active, but will be force killed and deleted.", name); - return; - } else { - return; - } - }, - Err(e) => { - eprintln!("Error occurred: {:?}", e); - }, - }; - process::exit(1); -} - -pub(crate) fn assert_session_ne(name: &str) { - if name.trim().is_empty() { - eprintln!("Session name cannot be empty. Please provide a specific session name."); - process::exit(1); - } - if name == "." || name == ".." { - eprintln!("Invalid session name: \"{}\".", name); - process::exit(1); - } - if name.contains('/') { - eprintln!("Session name cannot contain '/'."); - process::exit(1); - } - - match session_exists(name) { - Ok(result) if !result => { - let resurrectable_sessions = get_resurrectable_session_names(); - if resurrectable_sessions.iter().find(|s| s == &name).is_some() { - println!("Session with name {:?} already exists, but is dead. Use the attach command to resurrect it or, the delete-session command to kill it or specify a different name.", name); - } else { - return - } - } - Ok(_) => println!("Session with name {:?} already exists. Use attach command to connect to it or specify a different name.", name), - Err(e) => eprintln!("Error occurred: {:?}", e), - }; - process::exit(1); -} - -/// Create a new random name generator -/// -/// Used to provide a memorable handle for a session when users don't specify a session name when the session is -/// created. -/// -/// Uses the list of adjectives and nouns defined below, with the intention of avoiding unfortunate -/// and offensive combinations. Care should be taken when adding or removing to either list due to the birthday paradox/ -/// hash collisions, e.g. with 4096 unique names, the likelihood of a collision in 10 session names is 1%. -pub(crate) fn get_name_generator() -> impl Iterator<Item = String> { - names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain) -} - -const ADJECTIVES: &[&'static str] = &[ - "adamant", - "adept", - "adventurous", - "arcadian", - "auspicious", - "awesome", - "blossoming", - "brave", - "charming", - "chatty", - "circular", - "considerate", - "cubic", - "curious", - "delighted", - "didactic", - "diligent", - "effulgent", - "erudite", - "excellent", - "exquisite", - "fabulous", - "fascinating", - "friendly", - "glowing", - "gracious", - "gregarious", - "hopeful", - "implacable", - "inventive", - "joyous", - "judicious", - "jumping", - "kind", - "likable", - "loyal", - "lucky", - "marvellous", - "mellifluous", - "nautical", - "oblong", - "outstanding", - "polished", - "polite", - "profound", - "quadratic", - "quiet", - "rectangular", - "remarkable", - "rusty", - "sensible", - "sincere", - "sparkling", - "splendid", - "stellar", - "tenacious", - "tremendous", - "triangular", - "undulating", - "unflappable", - "unique", - "verdant", - "vitreous", - "wise", - "zippy", -]; - -const NOUNS: &[&'static str] = &[ - "aardvark", - "accordion", - "apple", - "apricot", - "bee", - "brachiosaur", - "cactus", - "capsicum", - "clarinet", - "cowbell", - "crab", - "cuckoo", - "cymbal", - "diplodocus", - "donkey", - "drum", - "duck", - "echidna", - "elephant", - "foxglove", - "galaxy", - "glockenspiel", - "goose", - "hill", - "horse", - "iguanadon", - "jellyfish", - "kangaroo", - "lake", - "lemon", - "lemur", - "magpie", - "megalodon", - "mountain", - "mouse", - "muskrat", - "newt", - "oboe", - "ocelot", - "orange", - "panda", - "peach", - "pepper", - "petunia", - "pheasant", - "piano", - "pigeon", - "platypus", - "quasar", - "rhinoceros", - "river", - "rustacean", - "salamander", - "sitar", - "stegosaurus", - "tambourine", - "tiger", - "tomato", - "triceratops", - "ukulele", - "viola", - "weasel", - "xylophone", - "yak", - "zebra", -]; |
