diff --git a/Cargo.lock b/Cargo.lock index 0dda83734..596062e17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2301,6 +2301,7 @@ name = "zellij" version = "0.12.0" dependencies = [ "insta", + "names", "zellij-client", "zellij-server", "zellij-utils", @@ -2358,8 +2359,8 @@ dependencies = [ "interprocess", "lazy_static", "libc", - "names", "nix", + "once_cell", "serde", "serde_yaml", "signal-hook 0.3.8", diff --git a/Cargo.toml b/Cargo.toml index 9e7e31ab4..5f7afaaa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ resolver = "2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +names = "0.11.0" zellij-client = { path = "zellij-client/", version = "0.12.0" } zellij-server = { path = "zellij-server/", version = "0.12.0" } zellij-utils = { path = "zellij-utils/", version = "0.12.0" } diff --git a/assets/config/default.yaml b/assets/config/default.yaml index 4582e79f6..42bc16886 100644 --- a/assets/config/default.yaml +++ b/assets/config/default.yaml @@ -12,6 +12,8 @@ keybinds: key: [Ctrl: 't',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's',] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [Quit,] key: [Ctrl: 'q',] - action: [NewPane: ] @@ -42,6 +44,8 @@ keybinds: key: [Ctrl: 'r', Char: "\n", Char: ' ',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's'] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [Quit] key: [Ctrl: 'q'] - action: [Resize: Left,] @@ -77,6 +81,8 @@ keybinds: key: [Ctrl: 'p', Char: "\n", Char: ' ',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's'] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [Quit,] key: [Ctrl: 'q',] - action: [MoveFocus: Left,] @@ -114,6 +120,8 @@ keybinds: key: [Ctrl: 't', Char: "\n", Char: ' ',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's'] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [SwitchToMode: RenameTab, TabNameInput: [0],] key: [Char: 'r'] - action: [Quit,] @@ -168,6 +176,8 @@ keybinds: key: [Ctrl: 'g',] - action: [SwitchToMode: Pane,] key: [Ctrl: 'p',] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [Quit,] key: [Ctrl: 'q',] - action: [ScrollDown,] @@ -213,3 +223,20 @@ keybinds: key: [ Alt: '[',] - action: [FocusNextPane,] key: [ Alt: ']',] + session: + - action: [SwitchToMode: Locked,] + key: [Ctrl: 'g'] + - action: [SwitchToMode: Resize,] + key: [Ctrl: 'r',] + - action: [SwitchToMode: Pane,] + key: [Ctrl: 'p',] + - action: [SwitchToMode: Tab,] + key: [Ctrl: 't',] + - action: [SwitchToMode: Normal,] + key: [Ctrl: 'o', Char: "\n", Char: ' ',] + - action: [SwitchToMode: Scroll,] + key: [Ctrl: 's'] + - action: [Quit,] + key: [Ctrl: 'q',] + - action: [Detach,] + key: [Char: 'd',] diff --git a/default-plugins/status-bar/src/first_line.rs b/default-plugins/status-bar/src/first_line.rs index 37175e3fa..7a678cdc4 100644 --- a/default-plugins/status-bar/src/first_line.rs +++ b/default-plugins/status-bar/src/first_line.rs @@ -22,6 +22,7 @@ enum CtrlKeyAction { Resize, Scroll, Quit, + Session, } enum CtrlKeyMode { @@ -39,16 +40,7 @@ impl CtrlKeyShortcut { CtrlKeyAction::Resize => String::from("RESIZE"), CtrlKeyAction::Scroll => String::from("SCROLL"), CtrlKeyAction::Quit => String::from("QUIT"), - } - } - pub fn shortened_text(&self) -> String { - match self.action { - CtrlKeyAction::Lock => String::from("LOCK"), - CtrlKeyAction::Pane => String::from("ane"), - CtrlKeyAction::Tab => String::from("ab"), - CtrlKeyAction::Resize => String::from("esize"), - CtrlKeyAction::Scroll => String::from("croll"), - CtrlKeyAction::Quit => String::from("uit"), + CtrlKeyAction::Session => String::from("SESSION"), } } pub fn letter_shortcut(&self) -> char { @@ -59,6 +51,7 @@ impl CtrlKeyShortcut { CtrlKeyAction::Resize => 'r', CtrlKeyAction::Scroll => 's', CtrlKeyAction::Quit => 'q', + CtrlKeyAction::Session => 'o', } } } @@ -193,32 +186,6 @@ fn full_ctrl_key(key: &CtrlKeyShortcut, palette: ColoredElements, separator: &st } } -fn shortened_ctrl_key( - key: &CtrlKeyShortcut, - palette: ColoredElements, - separator: &str, -) -> LinePart { - let shortened_text = key.shortened_text(); - let letter_shortcut = key.letter_shortcut(); - let shortened_text = match key.action { - CtrlKeyAction::Lock => format!(" {}", shortened_text), - _ => shortened_text, - }; - match key.mode { - CtrlKeyMode::Unselected => { - unselected_mode_shortcut(letter_shortcut, &shortened_text, palette, separator) - } - CtrlKeyMode::Selected => { - selected_mode_shortcut(letter_shortcut, &shortened_text, palette, separator) - } - CtrlKeyMode::Disabled => disabled_mode_shortcut( - &format!(" <{}>{}", letter_shortcut, shortened_text), - palette, - separator, - ), - } -} - fn single_letter_ctrl_key( key: &CtrlKeyShortcut, palette: ColoredElements, @@ -254,15 +221,6 @@ fn key_indicators( return line_part; } line_part = LinePart::default(); - for ctrl_key in keys { - let key = shortened_ctrl_key(ctrl_key, palette, separator); - line_part.part = format!("{}{}", line_part.part, key.part); - line_part.len += key.len; - } - if line_part.len < max_len { - return line_part; - } - line_part = LinePart::default(); for ctrl_key in keys { let key = single_letter_ctrl_key(ctrl_key, palette, separator); line_part.part = format!("{}{}", line_part.part, key.part); @@ -296,6 +254,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Tab), CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Resize), CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Scroll), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Quit), ], colored_elements, @@ -309,6 +268,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Resize), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), ], colored_elements, @@ -322,6 +282,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), ], colored_elements, @@ -335,6 +296,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Tab), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), ], colored_elements, @@ -348,6 +310,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize), CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Scroll), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), ], colored_elements, @@ -361,6 +324,21 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), + ], + colored_elements, + separator, + ), + InputMode::Session => key_indicators( + max_len, + &[ + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Pane), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize), + CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll), + CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Session), CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit), ], colored_elements, diff --git a/src/main.rs b/src/main.rs index a970ce446..67b833988 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ +mod sessions; #[cfg(test)] mod tests; +use sessions::{assert_session, assert_session_ne, list_sessions}; use std::convert::TryFrom; -use zellij_client::{os_input_output::get_client_os_input, start_client}; +use std::process; +use zellij_client::{os_input_output::get_client_os_input, start_client, ClientInfo}; use zellij_server::{os_input_output::get_server_os_input, start_server}; use zellij_utils::{ - cli::{CliArgs, ConfigCli}, + cli::{CliArgs, Command, Sessions}, consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR}, input::config::Config, logging::*, @@ -16,15 +19,17 @@ use zellij_utils::{ pub fn main() { let opts = CliArgs::from_args(); - if let Some(ConfigCli::Setup(setup)) = opts.option.clone() { - Setup::from_cli(&setup, &opts).expect("Failed to print to stdout"); + if let Some(Command::Sessions(Sessions::ListSessions)) = opts.command { + list_sessions(); + } else if let Some(Command::Setup(ref setup)) = opts.command { + Setup::from_cli(setup, &opts).expect("Failed to print to stdout"); } let config = match Config::try_from(&opts) { Ok(config) => config, Err(e) => { eprintln!("There was an error in the config file:\n{}", e); - std::process::exit(1); + process::exit(1); } }; atomic_create_dir(&*ZELLIJ_TMP_DIR).unwrap(); @@ -34,7 +39,7 @@ pub fn main() { Ok(server_os_input) => server_os_input, Err(e) => { eprintln!("failed to open terminal:\n{}", e); - std::process::exit(1); + process::exit(1); } }; start_server(Box::new(os_input), path); @@ -43,9 +48,33 @@ pub fn main() { Ok(os_input) => os_input, Err(e) => { eprintln!("failed to open terminal:\n{}", e); - std::process::exit(1); + process::exit(1); } }; - start_client(Box::new(os_input), opts, config); + if let Some(Command::Sessions(Sessions::Attach { + session_name, + force, + })) = opts.command.clone() + { + assert_session(&session_name); + start_client( + Box::new(os_input), + opts, + config, + ClientInfo::Attach(session_name, force), + ); + } else { + let session_name = opts + .session + .clone() + .unwrap_or_else(|| names::Generator::default().next().unwrap()); + assert_session_ne(&session_name); + start_client( + Box::new(os_input), + opts, + config, + ClientInfo::New(session_name), + ); + } } } diff --git a/src/sessions.rs b/src/sessions.rs new file mode 100644 index 000000000..fd834e408 --- /dev/null +++ b/src/sessions.rs @@ -0,0 +1,109 @@ +use std::os::unix::fs::FileTypeExt; +use std::{fs, io, process}; +use zellij_utils::{ + consts::ZELLIJ_SOCK_DIR, + interprocess::local_socket::LocalSocketStream, + ipc::{ClientToServerMsg, IpcSenderWithContext}, +}; + +fn get_sessions() -> Result, 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(); + if file.file_type().unwrap().is_socket() && assert_socket(&file_name) { + sessions.push(file_name); + } + }); + Ok(sessions) + } + Err(err) => { + if let io::ErrorKind::NotFound = err.kind() { + Ok(Vec::with_capacity(0)) + } else { + Err(err.kind()) + } + } + } +} + +pub(crate) fn list_sessions() { + let exit_code = match get_sessions() { + Ok(sessions) => { + if sessions.is_empty() { + println!("No active zellij sessions found."); + } else { + let curr_session = + std::env::var("ZELLIJ_SESSION_NAME").unwrap_or_else(|_| "".into()); + sessions.iter().for_each(|session| { + let suffix = if curr_session == *session { + " (current)" + } else { + "" + }; + println!("{}{}", session, suffix); + }) + } + 0 + } + Err(e) => { + eprintln!("Error occured: {:?}", e); + 1 + } + }; + process::exit(exit_code); +} + +pub(crate) fn assert_session(name: &str) { + let exit_code = match get_sessions() { + Ok(sessions) => { + if sessions.iter().any(|s| s == name) { + return; + } + println!("No session named {:?} found.", name); + 0 + } + Err(e) => { + eprintln!("Error occured: {:?}", e); + 1 + } + }; + process::exit(exit_code); +} + +pub(crate) fn assert_session_ne(name: &str) { + let exit_code = match get_sessions() { + Ok(sessions) => { + if sessions.iter().all(|s| s != name) { + return; + } + println!("Session with name {:?} aleady exists. Use attach command to connect to it or specify a different name.", name); + 0 + } + Err(e) => { + eprintln!("Error occured: {:?}", e); + 1 + } + }; + process::exit(exit_code); +} + +fn assert_socket(name: &str) -> bool { + let path = &*ZELLIJ_SOCK_DIR.join(name); + match LocalSocketStream::connect(path) { + Ok(stream) => { + IpcSenderWithContext::new(stream).send(ClientToServerMsg::ClientExited); + true + } + Err(e) => { + if e.kind() == io::ErrorKind::ConnectionRefused { + drop(fs::remove_file(path)); + false + } else { + true + } + } + } +} diff --git a/src/tests/fakes.rs b/src/tests/fakes.rs index a2f338ba6..23f9f1824 100644 --- a/src/tests/fakes.rs +++ b/src/tests/fakes.rs @@ -333,6 +333,8 @@ impl ServerOsApi for FakeInputOutput { self.send_instructions_to_client.send(msg).unwrap(); } fn add_client_sender(&self) {} + fn remove_client_sender(&self) {} + fn send_to_temp_client(&self, _msg: ServerToClientMsg) {} fn update_receiver(&mut self, _stream: LocalSocketStream) {} fn load_palette(&self) -> Palette { default_palette() diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 3e548db7c..e075ed49a 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -5,7 +5,7 @@ pub mod tty_inputs; pub mod utils; use std::path::PathBuf; -use zellij_client::{os_input_output::ClientOsApi, start_client}; +use zellij_client::{os_input_output::ClientOsApi, start_client, ClientInfo}; use zellij_server::{os_input_output::ServerOsApi, start_server}; use zellij_utils::{cli::CliArgs, input::config::Config}; @@ -21,6 +21,6 @@ pub fn start( start_server(server_os_input, PathBuf::from("")); }) .unwrap(); - start_client(client_os_input, opts, config); + start_client(client_os_input, opts, config, ClientInfo::New("".into())); let _ = server_thread.join(); } diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 4e2cff94c..70f6e9824 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -7,7 +7,7 @@ use zellij_utils::{ channels::{SenderWithContext, OPENCALLS}, errors::ContextType, input::{actions::Action, cast_termion_key, config::Config, keybinds::Keybinds}, - ipc::ClientToServerMsg, + ipc::{ClientToServerMsg, ExitReason}, }; use termion::input::TermReadEventsAndRaw; @@ -132,7 +132,9 @@ impl InputHandler { let mut should_break = false; match action { - Action::Quit => { + Action::Quit | Action::Detach => { + self.os_input + .send_to_server(ClientToServerMsg::Action(action)); self.exit(); should_break = true; } @@ -167,7 +169,7 @@ impl InputHandler { /// same as quitting Zellij). fn exit(&mut self) { self.send_client_instructions - .send(ClientInstruction::Exit) + .send(ClientInstruction::Exit(ExitReason::Normal)) .unwrap(); } } diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 9deb415e6..f3b0f11d3 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -17,30 +17,27 @@ use crate::{ use zellij_utils::cli::CliArgs; use zellij_utils::{ channels::{SenderType, SenderWithContext, SyncChannelWithContext}, - consts::ZELLIJ_IPC_PIPE, + consts::{SESSION_NAME, ZELLIJ_IPC_PIPE}, errors::{ClientContext, ContextType, ErrorInstruction}, - input::config::Config, - input::options::Options, - ipc::{ClientAttributes, ClientToServerMsg, ServerToClientMsg}, + input::{actions::Action, config::Config, options::Options}, + ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, }; /// Instructions related to the client-side application #[derive(Debug, Clone)] pub(crate) enum ClientInstruction { Error(String), - Render(Option), + Render(String), UnblockInputThread, - Exit, - ServerError(String), + Exit(ExitReason), } impl From for ClientInstruction { fn from(instruction: ServerToClientMsg) -> Self { match instruction { - ServerToClientMsg::Exit => ClientInstruction::Exit, + ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e), ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer), ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread, - ServerToClientMsg::ServerError(backtrace) => ClientInstruction::ServerError(backtrace), } } } @@ -48,9 +45,8 @@ impl From for ClientInstruction { impl From<&ClientInstruction> for ClientContext { fn from(client_instruction: &ClientInstruction) -> Self { match *client_instruction { - ClientInstruction::Exit => ClientContext::Exit, + ClientInstruction::Exit(_) => ClientContext::Exit, ClientInstruction::Error(_) => ClientContext::Error, - ClientInstruction::ServerError(_) => ClientContext::ServerError, ClientInstruction::Render(_) => ClientContext::Render, ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread, } @@ -80,7 +76,18 @@ fn spawn_server(socket_path: &Path) -> io::Result<()> { } } -pub fn start_client(mut os_input: Box, opts: CliArgs, config: Config) { +#[derive(Debug, Clone)] +pub enum ClientInfo { + Attach(String, bool), + New(String), +} + +pub fn start_client( + mut os_input: Box, + opts: CliArgs, + config: Config, + info: ClientInfo, +) { let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}12l\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l"; let take_snapshot = "\u{1b}[?1049h"; let bracketed_paste = "\u{1b}[?2004h"; @@ -96,24 +103,46 @@ pub fn start_client(mut os_input: Box, opts: CliArgs, config: C .unwrap(); std::env::set_var(&"ZELLIJ", "0"); - #[cfg(not(any(feature = "test", test)))] - spawn_server(&*ZELLIJ_IPC_PIPE).unwrap(); - - let mut command_is_executing = CommandIsExecuting::new(); - - let config_options = Options::from_cli(&config.options, opts.option.clone()); + let config_options = Options::from_cli(&config.options, opts.command.clone()); let full_screen_ws = os_input.get_terminal_size_using_fd(0); let client_attributes = ClientAttributes { position_and_size: full_screen_ws, palette, }; + + #[cfg(not(any(feature = "test", test)))] + let first_msg = match info { + ClientInfo::Attach(name, force) => { + SESSION_NAME.set(name).unwrap(); + std::env::set_var(&"ZELLIJ_SESSION_NAME", SESSION_NAME.get().unwrap()); + + ClientToServerMsg::AttachClient(client_attributes, force) + } + ClientInfo::New(name) => { + SESSION_NAME.set(name).unwrap(); + std::env::set_var(&"ZELLIJ_SESSION_NAME", SESSION_NAME.get().unwrap()); + + spawn_server(&*ZELLIJ_IPC_PIPE).unwrap(); + + ClientToServerMsg::NewClient( + client_attributes, + Box::new(opts), + Box::new(config_options), + ) + } + }; + #[cfg(any(feature = "test", test))] + let first_msg = { + let _ = SESSION_NAME.set("".into()); + ClientToServerMsg::NewClient(client_attributes, Box::new(opts), Box::new(config_options)) + }; + os_input.connect_to_server(&*ZELLIJ_IPC_PIPE); - os_input.send_to_server(ClientToServerMsg::NewClient( - client_attributes, - Box::new(opts), - Box::new(config_options), - )); + os_input.send_to_server(first_msg); + + let mut command_is_executing = CommandIsExecuting::new(); + os_input.set_raw_mode(0); let _ = os_input .get_stdout_writer() @@ -170,7 +199,7 @@ pub fn start_client(mut os_input: Box, opts: CliArgs, config: C let send_client_instructions = send_client_instructions.clone(); move || { send_client_instructions - .send(ClientInstruction::Exit) + .send(ClientInstruction::Exit(ExitReason::Normal)) .unwrap() } }), @@ -187,11 +216,8 @@ pub fn start_client(mut os_input: Box, opts: CliArgs, config: C move || loop { let (instruction, err_ctx) = os_input.recv_from_server(); err_ctx.update_thread_ctx(); - match instruction { - ServerToClientMsg::Exit | ServerToClientMsg::ServerError(_) => { - should_break = true; - } - _ => {} + if let ServerToClientMsg::Exit(_) = instruction { + should_break = true; } send_client_instructions.send(instruction.into()).unwrap(); if should_break { @@ -216,6 +242,8 @@ pub fn start_client(mut os_input: Box, opts: CliArgs, config: C std::process::exit(1); }; + let exit_msg: String; + loop { let (client_instruction, mut err_ctx) = receive_client_instructions .recv() @@ -223,21 +251,23 @@ pub fn start_client(mut os_input: Box, opts: CliArgs, config: C err_ctx.add_call(ContextType::Client((&client_instruction).into())); match client_instruction { - ClientInstruction::Exit => break, - ClientInstruction::Error(backtrace) => { - let _ = os_input.send_to_server(ClientToServerMsg::ClientExit); - handle_error(backtrace); + ClientInstruction::Exit(reason) => { + os_input.send_to_server(ClientToServerMsg::ClientExited); + + if let ExitReason::Error(_) = reason { + handle_error(format!("{}", reason)); + } + exit_msg = format!("{}", reason); + break; } - ClientInstruction::ServerError(backtrace) => { + ClientInstruction::Error(backtrace) => { + let _ = os_input.send_to_server(ClientToServerMsg::Action(Action::Quit)); handle_error(backtrace); } ClientInstruction::Render(output) => { - if output.is_none() { - break; - } let mut stdout = os_input.get_stdout_writer(); stdout - .write_all(&output.unwrap().as_bytes()) + .write_all(&output.as_bytes()) .expect("cannot write to stdout"); stdout.flush().expect("could not flush"); } @@ -247,7 +277,6 @@ pub fn start_client(mut os_input: Box, opts: CliArgs, config: C } } - let _ = os_input.send_to_server(ClientToServerMsg::ClientExit); router_thread.join().unwrap(); // cleanup(); @@ -256,8 +285,8 @@ pub fn start_client(mut os_input: Box, opts: CliArgs, config: C let restore_snapshot = "\u{1b}[?1049l"; let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); let goodbye_message = format!( - "{}\n{}{}{}Bye from Zellij!\n", - goto_start_of_last_line, restore_snapshot, reset_style, show_cursor + "{}\n{}{}{}{}\n", + goto_start_of_last_line, restore_snapshot, reset_style, show_cursor, exit_msg ); os_input.unset_raw_mode(0); diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index df0eea65f..40fb87f7d 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -11,11 +11,11 @@ mod wasm_vm; use zellij_utils::zellij_tile; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; use std::thread; use std::{path::PathBuf, sync::mpsc}; use wasmer::Store; -use zellij_tile::data::PluginCapabilities; +use zellij_tile::data::{Event, InputMode, PluginCapabilities}; use crate::{ os_input_output::ServerOsApi, @@ -30,8 +30,8 @@ use zellij_utils::{ channels::{ChannelWithContext, SenderType, SenderWithContext, SyncChannelWithContext}, cli::CliArgs, errors::{ContextType, ErrorInstruction, ServerContext}, - input::options::Options, - ipc::{ClientAttributes, ClientToServerMsg, ServerToClientMsg}, + input::{get_mode_info, options::Options}, + ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, setup::{get_default_data_dir, install::populate_data_dir}, }; @@ -43,14 +43,18 @@ pub(crate) enum ServerInstruction { UnblockInputThread, ClientExit, Error(String), + DetachSession, + AttachClient(ClientAttributes, bool), } impl From for ServerInstruction { fn from(instruction: ClientToServerMsg) -> Self { match instruction { - ClientToServerMsg::ClientExit => ServerInstruction::ClientExit, - ClientToServerMsg::NewClient(pos, opts, options) => { - ServerInstruction::NewClient(pos, opts, options) + ClientToServerMsg::NewClient(attrs, opts, options) => { + ServerInstruction::NewClient(attrs, opts, options) + } + ClientToServerMsg::AttachClient(attrs, force) => { + ServerInstruction::AttachClient(attrs, force) } _ => unreachable!(), } @@ -65,6 +69,8 @@ impl From<&ServerInstruction> for ServerContext { ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread, ServerInstruction::ClientExit => ServerContext::ClientExit, ServerInstruction::Error(_) => ServerContext::Error, + ServerInstruction::DetachSession => ServerContext::DetachSession, + ServerInstruction::AttachClient(..) => ServerContext::AttachClient, } } } @@ -94,6 +100,13 @@ impl Drop for SessionMetaData { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum SessionState { + Attached, + Detached, + Uninitialized, +} + pub fn start_server(os_input: Box, socket_path: PathBuf) { #[cfg(not(any(feature = "test", test)))] daemonize::Daemonize::new() @@ -107,7 +120,8 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { let (to_server, server_receiver): SyncChannelWithContext = mpsc::sync_channel(50); let to_server = SenderWithContext::new(SenderType::SyncSender(to_server)); - let sessions: Arc>> = Arc::new(RwLock::new(None)); + let session_data: Arc>> = Arc::new(RwLock::new(None)); + let session_state = Arc::new(RwLock::new(SessionState::Uninitialized)); #[cfg(not(any(feature = "test", test)))] std::panic::set_hook({ @@ -118,17 +132,22 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { }) }); - #[cfg(any(feature = "test", test))] - thread::Builder::new() - .name("server_router".to_string()) - .spawn({ - let sessions = sessions.clone(); - let os_input = os_input.clone(); - let to_server = to_server.clone(); + let thread_handles = Arc::new(Mutex::new(Vec::new())); - move || route_thread_main(sessions, os_input, to_server) - }) - .unwrap(); + #[cfg(any(feature = "test", test))] + thread_handles.lock().unwrap().push( + thread::Builder::new() + .name("server_router".to_string()) + .spawn({ + let session_data = session_data.clone(); + let os_input = os_input.clone(); + let to_server = to_server.clone(); + let session_state = session_state.clone(); + + move || route_thread_main(session_data, session_state, os_input, to_server) + }) + .unwrap(), + ); #[cfg(not(any(feature = "test", test)))] let _ = thread::Builder::new() .name("server_listener".to_string()) @@ -138,9 +157,11 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { }; let os_input = os_input.clone(); - let sessions = sessions.clone(); + let session_data = session_data.clone(); + let session_state = session_state.clone(); let to_server = to_server.clone(); let socket_path = socket_path.clone(); + let thread_handles = thread_handles.clone(); move || { drop(std::fs::remove_file(&socket_path)); let listener = LocalSocketListener::bind(&*socket_path).unwrap(); @@ -150,18 +171,28 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { Ok(stream) => { let mut os_input = os_input.clone(); os_input.update_receiver(stream); - let sessions = sessions.clone(); + let session_data = session_data.clone(); + let session_state = session_state.clone(); let to_server = to_server.clone(); - thread::Builder::new() - .name("server_router".to_string()) - .spawn({ - let sessions = sessions.clone(); - let os_input = os_input.clone(); - let to_server = to_server.clone(); + thread_handles.lock().unwrap().push( + thread::Builder::new() + .name("server_router".to_string()) + .spawn({ + let session_data = session_data.clone(); + let os_input = os_input.clone(); + let to_server = to_server.clone(); - move || route_thread_main(sessions, os_input, to_server) - }) - .unwrap(); + move || { + route_thread_main( + session_data, + session_state, + os_input, + to_server, + ) + } + }) + .unwrap(), + ); } Err(err) => { panic!("err {:?}", err); @@ -176,15 +207,17 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { err_ctx.add_call(ContextType::IPCServer((&instruction).into())); match instruction { ServerInstruction::NewClient(client_attributes, opts, config_options) => { - let session_data = init_session( + let session = init_session( os_input.clone(), opts, config_options, to_server.clone(), client_attributes, + session_state.clone(), ); - *sessions.write().unwrap() = Some(session_data); - sessions + *session_data.write().unwrap() = Some(session); + *session_state.write().unwrap() = SessionState::Attached; + session_data .read() .unwrap() .as_ref() @@ -193,23 +226,69 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { .send_to_pty(PtyInstruction::NewTab) .unwrap(); } + ServerInstruction::AttachClient(attrs, _) => { + *session_state.write().unwrap() = SessionState::Attached; + let rlock = session_data.read().unwrap(); + let session_data = rlock.as_ref().unwrap(); + session_data + .senders + .send_to_screen(ScreenInstruction::TerminalResize(attrs.position_and_size)) + .unwrap(); + let mode_info = + get_mode_info(InputMode::Normal, attrs.palette, session_data.capabilities); + session_data + .senders + .send_to_screen(ScreenInstruction::ChangeMode(mode_info.clone())) + .unwrap(); + session_data + .senders + .send_to_plugin(PluginInstruction::Update( + None, + Event::ModeUpdate(mode_info), + )) + .unwrap(); + } ServerInstruction::UnblockInputThread => { - os_input.send_to_client(ServerToClientMsg::UnblockInputThread); + if *session_state.read().unwrap() == SessionState::Attached { + os_input.send_to_client(ServerToClientMsg::UnblockInputThread); + } } ServerInstruction::ClientExit => { - *sessions.write().unwrap() = None; - os_input.send_to_client(ServerToClientMsg::Exit); + *session_data.write().unwrap() = None; + os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); break; } + ServerInstruction::DetachSession => { + *session_state.write().unwrap() = SessionState::Detached; + os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); + os_input.remove_client_sender(); + } ServerInstruction::Render(output) => { - os_input.send_to_client(ServerToClientMsg::Render(output)) + if *session_state.read().unwrap() == SessionState::Attached { + // Here output is of the type Option sent by screen thread. + // If `Some(_)`- unwrap it and forward it to the client to render. + // If `None`- Send an exit instruction. This is the case when the user closes last Tab/Pane. + if let Some(op) = output { + os_input.send_to_client(ServerToClientMsg::Render(op)); + } else { + os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); + break; + } + } } ServerInstruction::Error(backtrace) => { - os_input.send_to_client(ServerToClientMsg::ServerError(backtrace)); + if *session_state.read().unwrap() == SessionState::Attached { + os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Error(backtrace))); + } break; } } } + thread_handles + .lock() + .unwrap() + .drain(..) + .for_each(|h| drop(h.join())); #[cfg(not(any(feature = "test", test)))] drop(std::fs::remove_file(&socket_path)); } @@ -220,6 +299,7 @@ fn init_session( config_options: Box, to_server: SenderWithContext, client_attributes: ClientAttributes, + session_state: Arc>, ) -> SessionMetaData { let (to_screen, screen_receiver): ChannelWithContext = mpsc::channel(); let to_screen = SenderWithContext::new(SenderType::Sender(to_screen)); @@ -285,7 +365,13 @@ fn init_session( let max_panes = opts.max_panes; move || { - screen_thread_main(screen_bus, max_panes, client_attributes, config_options); + screen_thread_main( + screen_bus, + max_panes, + client_attributes, + config_options, + session_state, + ); } }) .unwrap(); diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 2ac35b69b..4cda73ed5 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -18,7 +18,10 @@ use signal_hook::consts::*; use zellij_tile::data::Palette; use zellij_utils::{ errors::ErrorContext, - ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg}, + ipc::{ + ClientToServerMsg, ExitReason, IpcReceiverWithContext, IpcSenderWithContext, + ServerToClientMsg, + }, shared::default_palette, }; @@ -189,6 +192,14 @@ pub trait ServerOsApi: Send + Sync { fn send_to_client(&self, msg: ServerToClientMsg); /// Adds a sender to client fn add_client_sender(&self); + /// Send to the temporary client + // A temporary client is the one that hasn't been registered as a client yet. + // Only the corresponding router thread has access to send messages to it. + // This can be the case when the client cannot attach to the session, + // so it tries to connect and then exits, hence temporary. + fn send_to_temp_client(&self, msg: ServerToClientMsg); + /// Removes the sender to client + fn remove_client_sender(&self); /// Update the receiver socket for the client fn update_receiver(&mut self, stream: LocalSocketStream); fn load_palette(&self) -> Palette; @@ -245,7 +256,6 @@ impl ServerOsApi for ServerOsInputOutput { .send(msg); } fn add_client_sender(&self) { - assert!(self.send_instructions_to_client.lock().unwrap().is_none()); let sender = self .receive_instructions_from_client .as_ref() @@ -253,7 +263,27 @@ impl ServerOsApi for ServerOsInputOutput { .lock() .unwrap() .get_sender(); - *self.send_instructions_to_client.lock().unwrap() = Some(sender); + let old_sender = self + .send_instructions_to_client + .lock() + .unwrap() + .replace(sender); + if let Some(mut sender) = old_sender { + sender.send(ServerToClientMsg::Exit(ExitReason::ForceDetached)); + } + } + fn send_to_temp_client(&self, msg: ServerToClientMsg) { + self.receive_instructions_from_client + .as_ref() + .unwrap() + .lock() + .unwrap() + .get_sender() + .send(msg); + } + fn remove_client_sender(&self) { + assert!(self.send_instructions_to_client.lock().unwrap().is_some()); + *self.send_instructions_to_client.lock().unwrap() = None; } fn update_receiver(&mut self, stream: LocalSocketStream) { self.receive_instructions_from_client = diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 0187d9536..8dcd98ffa 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -4,7 +4,7 @@ use zellij_utils::zellij_tile::data::Event; use crate::{ os_input_output::ServerOsApi, pty::PtyInstruction, screen::ScreenInstruction, - wasm_vm::PluginInstruction, ServerInstruction, SessionMetaData, + wasm_vm::PluginInstruction, ServerInstruction, SessionMetaData, SessionState, }; use zellij_utils::{ channels::SenderWithContext, @@ -12,10 +12,16 @@ use zellij_utils::{ actions::{Action, Direction}, get_mode_info, }, - ipc::ClientToServerMsg, + ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg}, }; -fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn ServerOsApi) { +fn route_action( + action: Action, + session: &SessionMetaData, + os_input: &dyn ServerOsApi, + to_server: &SenderWithContext, +) -> bool { + let mut should_break = false; match action { Action::Write(val) => { session @@ -182,28 +188,36 @@ fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn Server .send_to_screen(ScreenInstruction::UpdateTabName(c)) .unwrap(); } + Action::Quit => { + to_server.send(ServerInstruction::ClientExit).unwrap(); + should_break = true; + } + Action::Detach => { + to_server.send(ServerInstruction::DetachSession).unwrap(); + should_break = true; + } Action::NoOp => {} - Action::Quit => panic!("Received unexpected action"), } + should_break } pub(crate) fn route_thread_main( - sessions: Arc>>, + session_data: Arc>>, + session_state: Arc>, os_input: Box, to_server: SenderWithContext, ) { loop { let (instruction, err_ctx) = os_input.recv_from_client(); err_ctx.update_thread_ctx(); - let rlocked_sessions = sessions.read().unwrap(); + let rlocked_sessions = session_data.read().unwrap(); + match instruction { - ClientToServerMsg::ClientExit => { - to_server.send(instruction.into()).unwrap(); - break; - } ClientToServerMsg::Action(action) => { if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { - route_action(action, rlocked_sessions, &*os_input); + if route_action(action, rlocked_sessions, &*os_input, &to_server) { + break; + } } } ClientToServerMsg::TerminalResize(new_size) => { @@ -215,9 +229,24 @@ pub(crate) fn route_thread_main( .unwrap(); } ClientToServerMsg::NewClient(..) => { - os_input.add_client_sender(); - to_server.send(instruction.into()).unwrap(); + if *session_state.read().unwrap() != SessionState::Uninitialized { + os_input.send_to_temp_client(ServerToClientMsg::Exit(ExitReason::Error( + "Cannot add new client".into(), + ))); + } else { + os_input.add_client_sender(); + to_server.send(instruction.into()).unwrap(); + } } + ClientToServerMsg::AttachClient(_, force) => { + if *session_state.read().unwrap() == SessionState::Attached && !force { + os_input.send_to_temp_client(ServerToClientMsg::Exit(ExitReason::CannotAttach)); + } else { + os_input.add_client_sender(); + to_server.send(instruction.into()).unwrap(); + } + } + ClientToServerMsg::ClientExited => break, } } } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 830c42511..068037eea 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use std::os::unix::io::RawFd; use std::str; +use std::sync::{Arc, RwLock}; use zellij_utils::zellij_tile; @@ -13,7 +14,7 @@ use crate::{ thread_bus::Bus, ui::layout::Layout, wasm_vm::PluginInstruction, - ServerInstruction, + ServerInstruction, SessionState, }; use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, PluginCapabilities, TabInfo}; use zellij_utils::{ @@ -137,6 +138,7 @@ pub(crate) struct Screen { mode_info: ModeInfo, input_mode: InputMode, colors: Palette, + session_state: Arc>, } impl Screen { @@ -147,6 +149,7 @@ impl Screen { max_panes: Option, mode_info: ModeInfo, input_mode: InputMode, + session_state: Arc>, ) -> Self { Screen { bus, @@ -157,6 +160,7 @@ impl Screen { tabs: BTreeMap::new(), mode_info, input_mode, + session_state, } } @@ -177,6 +181,7 @@ impl Screen { self.mode_info.clone(), self.input_mode, self.colors, + self.session_state.clone(), ); self.active_tab_index = Some(tab_index); self.tabs.insert(tab_index, tab); @@ -261,10 +266,12 @@ impl Screen { .unwrap(); if self.tabs.is_empty() { self.active_tab_index = None; - self.bus - .senders - .send_to_server(ServerInstruction::Render(None)) - .unwrap(); + if *self.session_state.read().unwrap() == SessionState::Attached { + self.bus + .senders + .send_to_server(ServerInstruction::Render(None)) + .unwrap(); + } } else { for t in self.tabs.values_mut() { if t.position > active_tab.position { @@ -286,6 +293,9 @@ impl Screen { /// Renders this [`Screen`], which amounts to rendering its active [`Tab`]. pub fn render(&mut self) { + if *self.session_state.read().unwrap() != SessionState::Attached { + return; + } if let Some(active_tab) = self.get_active_tab_mut() { if active_tab.get_active_pane().is_some() { active_tab.render(); @@ -333,6 +343,7 @@ impl Screen { self.mode_info.clone(), self.input_mode, self.colors, + self.session_state.clone(), ); tab.apply_layout(layout, new_pids); self.active_tab_index = Some(tab_index); @@ -375,6 +386,7 @@ impl Screen { self.update_tabs(); } pub fn change_mode(&mut self, mode_info: ModeInfo) { + self.colors = mode_info.palette; self.mode_info = mode_info; for tab in self.tabs.values_mut() { tab.mode_info = self.mode_info.clone(); @@ -390,6 +402,7 @@ pub(crate) fn screen_thread_main( max_panes: Option, client_attributes: ClientAttributes, config_options: Box, + session_state: Arc>, ) { let capabilities = config_options.simplified_ui; @@ -405,6 +418,7 @@ pub(crate) fn screen_thread_main( ..ModeInfo::default() }, InputMode::Normal, + session_state, ); loop { let (event, mut err_ctx) = screen diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index e6953bfa2..4a0db153d 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -10,11 +10,11 @@ use crate::{ thread_bus::ThreadSenders, ui::{boundaries::Boundaries, layout::Layout, pane_resizer::PaneResizer}, wasm_vm::PluginInstruction, - ServerInstruction, + ServerInstruction, SessionState, }; use serde::{Deserialize, Serialize}; use std::os::unix::io::RawFd; -use std::sync::mpsc::channel; +use std::sync::{mpsc::channel, Arc, RwLock}; use std::time::Instant; use std::{ cmp::Reverse, @@ -74,6 +74,7 @@ pub(crate) struct Tab { pub senders: ThreadSenders, synchronize_is_active: bool, should_clear_display_before_rendering: bool, + session_state: Arc>, pub mode_info: ModeInfo, pub input_mode: InputMode, pub colors: Palette, @@ -242,6 +243,7 @@ impl Tab { mode_info: ModeInfo, input_mode: InputMode, colors: Palette, + session_state: Arc>, ) -> Self { let panes = if let Some(PaneId::Terminal(pid)) = pane_id { let new_terminal = TerminalPane::new(pid, *full_screen_ws, colors); @@ -273,6 +275,7 @@ impl Tab { mode_info, input_mode, colors, + session_state, } } @@ -722,9 +725,12 @@ impl Tab { self.panes.iter().any(|(_, p)| p.contains_widechar()) } pub fn render(&mut self) { - if self.active_terminal.is_none() { + if self.active_terminal.is_none() + || *self.session_state.read().unwrap() != SessionState::Attached + { // we might not have an active terminal if we closed the last pane // in that case, we should not render as the app is exiting + // or if this session is not attached to a client, we do not have to render return; } // if any pane contain widechar, all pane in the same row will messup. We should render them every time diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index c1b9c30ef..cb4b2aa01 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -59,6 +59,9 @@ pub enum InputMode { Scroll, #[serde(alias = "renametab")] RenameTab, + /// `Session` mode allows detaching sessions + #[serde(alias = "session")] + Session, } impl Default for InputMode { diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index b1f48d10b..2fcccd7d7 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -16,8 +16,8 @@ directories-next = "2.0" interprocess = "1.1.1" lazy_static = "1.4.0" libc = "0.2" -names = "0.11.0" nix = "0.19.1" +once_cell = "1.7.2" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" signal-hook = "0.3" diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 4b2fdbbfb..bf7851c9f 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -17,9 +17,13 @@ pub struct CliArgs { pub data_dir: Option, /// Run server listening at the specified socket path - #[structopt(long, parse(from_os_str))] + #[structopt(long, parse(from_os_str), hidden = true)] pub server: Option, + /// Specify name of a new session + #[structopt(long, short)] + pub session: Option, + /// Name of a layout file in the layout directory #[structopt(short, long, parse(from_os_str))] pub layout: Option, @@ -37,14 +41,14 @@ pub struct CliArgs { pub config_dir: Option, #[structopt(subcommand)] - pub option: Option, + pub command: Option, #[structopt(short, long)] pub debug: bool, } #[derive(Debug, StructOpt, Clone, Serialize, Deserialize)] -pub enum ConfigCli { +pub enum Command { /// Change the behaviour of zellij #[structopt(name = "options")] Options(Options), @@ -52,4 +56,27 @@ pub enum ConfigCli { /// Setup zellij and check its configuration #[structopt(name = "setup")] Setup(Setup), + + /// Explore existing zellij sessions + #[structopt(flatten)] + Sessions(Sessions), +} + +#[derive(Debug, StructOpt, Clone, Serialize, Deserialize)] +pub enum Sessions { + /// List active sessions + #[structopt(alias = "ls")] + ListSessions, + + /// Attach to session + #[structopt(alias = "a")] + Attach { + /// Name of the session to attach to. + session_name: String, + + /// Force attach- session will detach from the other + /// zellij client (if any) and attach to this. + #[structopt(long, short)] + force: bool, + }, } diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs index 799e29132..f22dd4893 100644 --- a/zellij-utils/src/consts.rs +++ b/zellij-utils/src/consts.rs @@ -4,6 +4,7 @@ use crate::shared::set_permissions; use directories_next::ProjectDirs; use lazy_static::lazy_static; use nix::unistd::Uid; +use once_cell::sync::OnceCell; use std::path::PathBuf; use std::{env, fs}; @@ -24,10 +25,10 @@ const fn system_default_data_dir() -> &'static str { lazy_static! { static ref UID: Uid = Uid::current(); - pub static ref SESSION_NAME: String = names::Generator::default().next().unwrap(); + pub static ref SESSION_NAME: OnceCell = OnceCell::new(); pub static ref ZELLIJ_PROJ_DIR: ProjectDirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); - pub static ref ZELLIJ_IPC_PIPE: PathBuf = { + pub static ref ZELLIJ_SOCK_DIR: PathBuf = { let mut ipc_dir = env::var("ZELLIJ_SOCKET_DIR").map_or_else( |_| { ZELLIJ_PROJ_DIR @@ -37,11 +38,15 @@ lazy_static! { PathBuf::from, ); ipc_dir.push(VERSION); - fs::create_dir_all(&ipc_dir).unwrap(); - set_permissions(&ipc_dir).unwrap(); - ipc_dir.push(&*SESSION_NAME); ipc_dir }; + pub static ref ZELLIJ_IPC_PIPE: PathBuf = { + let mut sock_dir = ZELLIJ_SOCK_DIR.clone(); + fs::create_dir_all(&sock_dir).unwrap(); + set_permissions(&sock_dir).unwrap(); + sock_dir.push(SESSION_NAME.get().unwrap()); + sock_dir + }; pub static ref ZELLIJ_TMP_DIR: PathBuf = PathBuf::from("/tmp/zellij-".to_string() + &format!("{}", *UID)); pub static ref ZELLIJ_TMP_LOG_DIR: PathBuf = ZELLIJ_TMP_DIR.join("zellij-log"); diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 94a894c46..b47ddb9a2 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -260,4 +260,6 @@ pub enum ServerContext { UnblockInputThread, ClientExit, Error, + DetachSession, + AttachClient, } diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 5ad771da6..4e0ce0cb0 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -65,4 +65,6 @@ pub enum Action { CloseTab, GoToTab(u32), TabNameInput(Vec), + /// Detach session and exit + Detach, } diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index a03416890..c4cf0cd72 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use super::keybinds::{Keybinds, KeybindsFromYaml}; use super::options::Options; -use crate::cli::{CliArgs, ConfigCli}; +use crate::cli::{CliArgs, Command}; use crate::setup; use serde::{Deserialize, Serialize}; @@ -60,7 +60,7 @@ impl TryFrom<&CliArgs> for Config { return Config::new(&path); } - if let Some(ConfigCli::Setup(setup)) = opts.option.clone() { + if let Some(Command::Setup(ref setup)) = opts.command { if setup.clean { return Config::from_default_assets(); } @@ -179,7 +179,7 @@ mod config_test { fn try_from_cli_args_with_option_clean() { use crate::setup::Setup; let mut opts = CliArgs::default(); - opts.option = Some(ConfigCli::Setup(Setup { + opts.command = Some(Command::Setup(Setup { clean: true, ..Setup::default() })); diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 2747205b6..068e22bc4 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -45,6 +45,9 @@ pub fn get_mode_info( InputMode::RenameTab => { keybinds.push(("Enter".to_string(), "when done".to_string())); } + InputMode::Session => { + keybinds.push(("d".to_string(), "Detach".to_string())); + } } ModeInfo { mode, diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 33625b654..f9724c20f 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -1,5 +1,5 @@ //! Handles cli and configuration options -use crate::cli::ConfigCli; +use crate::cli::Command; use serde::{Deserialize, Serialize}; use structopt::StructOpt; @@ -35,8 +35,8 @@ impl Options { Options { simplified_ui } } - pub fn from_cli(&self, other: Option) -> Options { - if let Some(ConfigCli::Options(options)) = other { + pub fn from_cli(&self, other: Option) -> Options { + if let Some(Command::Options(options)) = other { Options::merge(&self, options) } else { self.to_owned() diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 725bf16b0..a160b782f 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -9,6 +9,7 @@ use crate::{ use interprocess::local_socket::LocalSocketStream; use nix::unistd::dup; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Error, Formatter}; use std::io::{self, Write}; use std::marker::PhantomData; use std::os::unix::io::{AsRawFd, FromRawFd}; @@ -34,7 +35,7 @@ pub enum ClientType { Writer, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub struct ClientAttributes { pub position_and_size: PositionAndSize, pub palette: Palette, @@ -54,10 +55,11 @@ pub enum ClientToServerMsg { DetachSession(SessionId), // Disconnect from the session we're connected to DisconnectFromSession,*/ - ClientExit, TerminalResize(PositionAndSize), NewClient(ClientAttributes, Box, Box), + AttachClient(ClientAttributes, bool), Action(Action), + ClientExited, } // Types of messages sent from the server to the client @@ -67,10 +69,34 @@ pub enum ServerToClientMsg { SessionInfo(Session), // A list of sessions SessionList(HashSet),*/ - Render(Option), + Render(String), UnblockInputThread, - Exit, - ServerError(String), + Exit(ExitReason), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ExitReason { + Normal, + ForceDetached, + CannotAttach, + Error(String), +} + +impl Display for ExitReason { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + Self::Normal => write!(f, "Bye from Zellij!"), + Self::ForceDetached => write!( + f, + "Session was detach from this client (possibly because another client connected)" + ), + Self::CannotAttach => write!( + f, + "Session attached to another client. Use --force flag to force connect." + ), + Self::Error(e) => write!(f, "Error occured in server:\n{}", e), + } + } } /// Sends messages on a stream socket, along with an [`ErrorContext`].