aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-06-23 19:19:37 +0200
committerGitHub <noreply@github.com>2025-06-23 19:19:37 +0200
commitc5ac7968809ddb1033b625562eae22019cccc2f4 (patch)
tree932cf07d898dfbcc6ec6a9d4cf5353c06a47accc /src
parentfeat(ui): add optional tooltip with key hints to the compact bar (#4225) (diff)
downloadsl0ppij-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.rs227
-rw-r--r--src/main.rs93
-rw-r--r--src/sessions.rs600
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",
-];