diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-08-24 13:36:24 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-24 13:36:24 +0200 |
commit | bc628abc1266cdc0dbce4f19a89727527a3e39a8 (patch) | |
tree | 6a0fcac6ec35c71a237bca0128a57a5b40afb0e3 /zellij-client | |
parent | bf3c072d6dd68da0abd838e95e6004091a4cd331 (diff) |
feat(sessions): add a session manager to switch between sessions, tabs and panes and create new ones (#2721)
* write/read session metadata to disk for all sessions
* switch session client side
* fix tests
* various adjustments
* fix full screen focus bug in tiled panes
* fix tests
* fix permission sorting issue
* cleanups
* add session manager
* fix tests
* various cleanups
* style(fmt): rustfmt
* clear screen before switching sessions
* I hate you clippy
* truncate controls line to width
* version session cache
* attempt to fix plugin tests
* style(fmt): rustfmt
* another attempt to fix the tests in the ci
Diffstat (limited to 'zellij-client')
-rw-r--r-- | zellij-client/src/input_handler.rs | 3 | ||||
-rw-r--r-- | zellij-client/src/lib.rs | 116 | ||||
-rw-r--r-- | zellij-client/src/os_input_output.rs | 59 | ||||
-rw-r--r-- | zellij-client/src/stdin_handler.rs | 117 | ||||
-rw-r--r-- | zellij-client/src/unit/stdin_tests.rs | 5 |
5 files changed, 203 insertions, 97 deletions
diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 3efe6a722..af977c796 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -157,6 +157,9 @@ impl InputHandler { .send(ClientInstruction::DoneParsingStdinQuery) .unwrap(); }, + Ok((InputInstruction::Exit, _error_context)) => { + self.should_exit = true; + }, Err(err) => panic!("Encountered read error: {:?}", err), } } diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index fb58db275..8dbae4f0a 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -23,8 +23,8 @@ use crate::{ }; use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, - consts::ZELLIJ_IPC_PIPE, - data::{ClientId, InputMode, Style}, + consts::{set_permissions, ZELLIJ_SOCK_DIR}, + data::{ClientId, ConnectToSession, InputMode, Style}, envs, errors::{ClientContext, ContextType, ErrorInstruction}, input::{config::Config, options::Options}, @@ -46,6 +46,7 @@ pub(crate) enum ClientInstruction { StartedParsingStdinQuery, DoneParsingStdinQuery, Log(Vec<String>), + SwitchSession(ConnectToSession), } impl From<ServerToClientMsg> for ClientInstruction { @@ -60,6 +61,9 @@ impl From<ServerToClientMsg> for ClientInstruction { ServerToClientMsg::Connected => ClientInstruction::Connected, ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients), ServerToClientMsg::Log(log_lines) => ClientInstruction::Log(log_lines), + ServerToClientMsg::SwitchSession(connect_to_session) => { + ClientInstruction::SwitchSession(connect_to_session) + }, } } } @@ -77,6 +81,7 @@ impl From<&ClientInstruction> for ClientContext { ClientInstruction::Log(_) => ClientContext::Log, ClientInstruction::StartedParsingStdinQuery => ClientContext::StartedParsingStdinQuery, ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery, + ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession, } } } @@ -130,6 +135,7 @@ pub(crate) enum InputInstruction { AnsiStdinInstructions(Vec<AnsiStdinInstruction>), StartedParsing, DoneParsing, + Exit, } pub fn start_client( @@ -139,8 +145,12 @@ pub fn start_client( config_options: Options, info: ClientInfo, layout: Option<Layout>, -) { + tab_position_to_focus: Option<usize>, + pane_id_to_focus: Option<(u32, bool)>, // (pane_id, is_plugin) +) -> Option<ConnectToSession> { info!("Starting Zellij client!"); + + let mut reconnect_to_session = None; let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\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"; @@ -172,28 +182,51 @@ pub fn start_client( keybinds: config.keybinds.clone(), }; - let first_msg = match info { + let create_ipc_pipe = || -> std::path::PathBuf { + let mut sock_dir = ZELLIJ_SOCK_DIR.clone(); + std::fs::create_dir_all(&sock_dir).unwrap(); + set_permissions(&sock_dir, 0o700).unwrap(); + sock_dir.push(envs::get_session_name().unwrap()); + sock_dir + }; + + let (first_msg, ipc_pipe) = match info { ClientInfo::Attach(name, config_options) => { - envs::set_session_name(name); + envs::set_session_name(name.clone()); + os_input.update_session_name(name); + let ipc_pipe = create_ipc_pipe(); - ClientToServerMsg::AttachClient(client_attributes, config_options) + ( + ClientToServerMsg::AttachClient( + client_attributes, + config_options, + tab_position_to_focus, + pane_id_to_focus, + ), + ipc_pipe, + ) }, ClientInfo::New(name) => { - envs::set_session_name(name); - - spawn_server(&*ZELLIJ_IPC_PIPE, opts.debug).unwrap(); - - ClientToServerMsg::NewClient( - client_attributes, - Box::new(opts), - Box::new(config_options.clone()), - Box::new(layout.unwrap()), - Some(config.plugins.clone()), + envs::set_session_name(name.clone()); + os_input.update_session_name(name); + let ipc_pipe = create_ipc_pipe(); + + spawn_server(&*ipc_pipe, opts.debug).unwrap(); + + ( + ClientToServerMsg::NewClient( + client_attributes, + Box::new(opts), + Box::new(config_options.clone()), + Box::new(layout.unwrap()), + Some(config.plugins.clone()), + ), + ipc_pipe, ) }, }; - os_input.connect_to_server(&*ZELLIJ_IPC_PIPE); + os_input.connect_to_server(&*ipc_pipe); os_input.send_to_server(first_msg); let mut command_is_executing = CommandIsExecuting::new(); @@ -336,7 +369,7 @@ pub fn start_client( std::process::exit(1); }; - let exit_msg: String; + let mut exit_msg = String::new(); let mut loading = true; let mut pending_instructions = vec![]; @@ -415,28 +448,43 @@ pub fn start_client( log::info!("{line}"); } }, + ClientInstruction::SwitchSession(connect_to_session) => { + reconnect_to_session = Some(connect_to_session); + os_input.send_to_server(ClientToServerMsg::ClientExited); + break; + }, _ => {}, } } router_thread.join().unwrap(); - // cleanup(); - let reset_style = "\u{1b}[m"; - let show_cursor = "\u{1b}[?25h"; - 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{}{}{}{}\n", - goto_start_of_last_line, restore_snapshot, reset_style, show_cursor, exit_msg - ); - - os_input.disable_mouse().non_fatal(); - info!("{}", exit_msg); - os_input.unset_raw_mode(0).unwrap(); - let mut stdout = os_input.get_stdout_writer(); - let _ = stdout.write(goodbye_message.as_bytes()).unwrap(); - stdout.flush().unwrap(); + if reconnect_to_session.is_none() { + let reset_style = "\u{1b}[m"; + let show_cursor = "\u{1b}[?25h"; + 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{}{}{}{}\n", + goto_start_of_last_line, restore_snapshot, reset_style, show_cursor, exit_msg + ); + + os_input.disable_mouse().non_fatal(); + info!("{}", exit_msg); + os_input.unset_raw_mode(0).unwrap(); + let mut stdout = os_input.get_stdout_writer(); + let _ = stdout.write(goodbye_message.as_bytes()).unwrap(); + stdout.flush().unwrap(); + } else { + let clear_screen = "\u{1b}[2J"; + let mut stdout = os_input.get_stdout_writer(); + let _ = stdout.write(clear_screen.as_bytes()).unwrap(); + stdout.flush().unwrap(); + } + + let _ = send_input_instructions.send(InputInstruction::Exit); + + reconnect_to_session } #[cfg(test)] diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs index 192c2159d..212ed2f7d 100644 --- a/zellij-client/src/os_input_output.rs +++ b/zellij-client/src/os_input_output.rs @@ -78,6 +78,8 @@ pub struct ClientOsInputOutput { orig_termios: Option<Arc<Mutex<termios::Termios>>>, send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>, receive_instructions_from_server: Arc<Mutex<Option<IpcReceiverWithContext<ServerToClientMsg>>>>, + reading_from_stdin: Arc<Mutex<Option<Vec<u8>>>>, + session_name: Arc<Mutex<Option<String>>>, } /// The `ClientOsApi` trait represents an abstract interface to the features of an operating system that @@ -94,8 +96,9 @@ pub trait ClientOsApi: Send + Sync { /// Returns the writer that allows writing to standard output. fn get_stdout_writer(&self) -> Box<dyn io::Write>; fn get_stdin_reader(&self) -> Box<dyn io::Read>; + fn update_session_name(&mut self, new_session_name: String); /// Returns the raw contents of standard input. - fn read_from_stdin(&mut self) -> Vec<u8>; + fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>; /// Returns a [`Box`] pointer to this [`ClientOsApi`] struct. fn box_clone(&self) -> Box<dyn ClientOsApi>; /// Sends a message to the server. @@ -135,14 +138,46 @@ impl ClientOsApi for ClientOsInputOutput { fn box_clone(&self) -> Box<dyn ClientOsApi> { Box::new((*self).clone()) } - fn read_from_stdin(&mut self) -> Vec<u8> { - let stdin = std::io::stdin(); - let mut stdin = stdin.lock(); - let buffer = stdin.fill_buf().unwrap(); - let length = buffer.len(); - let read_bytes = Vec::from(buffer); - stdin.consume(length); - read_bytes + fn update_session_name(&mut self, new_session_name: String) { + *self.session_name.lock().unwrap() = Some(new_session_name); + } + fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> { + let session_name_at_calltime = { self.session_name.lock().unwrap().clone() }; + // here we wait for a lock in case another thread is holding stdin + // this can happen for example when switching sessions, the old thread will only be + // released once it sees input over STDIN + // + // when this happens, we detect in the other thread that our session is ended (by comparing + // the session name at the beginning of the call and the one after we read from STDIN), and + // so place what we read from STDIN inside a buffer (the "reading_from_stdin" on our state) + // and release the lock + // + // then, another thread will see there's something in the buffer immediately as it acquires + // the lock (without having to wait for STDIN itself) forward this buffer and proceed to + // wait for the "real" STDIN net time it is called + let mut buffered_bytes = self.reading_from_stdin.lock().unwrap(); + match buffered_bytes.take() { + Some(buffered_bytes) => Ok(buffered_bytes), + None => { + let stdin = std::io::stdin(); + let mut stdin = stdin.lock(); + let buffer = stdin.fill_buf().unwrap(); + let length = buffer.len(); + let read_bytes = Vec::from(buffer); + stdin.consume(length); + + let session_name_after_reading_from_stdin = + { self.session_name.lock().unwrap().clone() }; + if session_name_at_calltime.is_some() + && session_name_at_calltime != session_name_after_reading_from_stdin + { + *buffered_bytes = Some(read_bytes); + Err("Session ended") + } else { + Ok(read_bytes) + } + }, + } } fn get_stdout_writer(&self) -> Box<dyn io::Write> { let stdout = ::std::io::stdout(); @@ -258,19 +293,25 @@ impl Clone for Box<dyn ClientOsApi> { pub fn get_client_os_input() -> Result<ClientOsInputOutput, nix::Error> { let current_termios = termios::tcgetattr(0)?; let orig_termios = Some(Arc::new(Mutex::new(current_termios))); + let reading_from_stdin = Arc::new(Mutex::new(None)); Ok(ClientOsInputOutput { orig_termios, send_instructions_to_server: Arc::new(Mutex::new(None)), receive_instructions_from_server: Arc::new(Mutex::new(None)), + reading_from_stdin, + session_name: Arc::new(Mutex::new(None)), }) } pub fn get_cli_client_os_input() -> Result<ClientOsInputOutput, nix::Error> { let orig_termios = None; // not a terminal + let reading_from_stdin = Arc::new(Mutex::new(None)); Ok(ClientOsInputOutput { orig_termios, send_instructions_to_server: Arc::new(Mutex::new(None)), receive_instructions_from_server: Arc::new(Mutex::new(None)), + reading_from_stdin, + session_name: Arc::new(Mutex::new(None)), }) } diff --git a/zellij-client/src/stdin_handler.rs b/zellij-client/src/stdin_handler.rs index 01f1ab232..eae0958ba 100644 --- a/zellij-client/src/stdin_handler.rs +++ b/zellij-client/src/stdin_handler.rs @@ -59,66 +59,79 @@ pub(crate) fn stdin_loop( } let mut ansi_stdin_events = vec![]; loop { - let buf = os_input.read_from_stdin(); - { - // here we check if we need to parse specialized ANSI instructions sent over STDIN - // this happens either on startup (see above) or on SIGWINCH - // - // if we need to parse them, we do so with an internal timeout - anything else we - // receive on STDIN during that timeout is unceremoniously dropped - let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap(); - if stdin_ansi_parser.should_parse() { - let events = stdin_ansi_parser.parse(buf); - if !events.is_empty() { - ansi_stdin_events.append(&mut events.clone()); - let _ = send_input_instructions - .send(InputInstruction::AnsiStdinInstructions(events)); + match os_input.read_from_stdin() { + Ok(buf) => { + { + // here we check if we need to parse specialized ANSI instructions sent over STDIN + // this happens either on startup (see above) or on SIGWINCH + // + // if we need to parse them, we do so with an internal timeout - anything else we + // receive on STDIN during that timeout is unceremoniously dropped + let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap(); + if stdin_ansi_parser.should_parse() { + let events = stdin_ansi_parser.parse(buf); + if !events.is_empty() { + ansi_stdin_events.append(&mut events.clone()); + let _ = send_input_instructions + .send(InputInstruction::AnsiStdinInstructions(events)); + } + continue; + } } - continue; - } - } - if !ansi_stdin_events.is_empty() { - stdin_ansi_parser - .lock() - .unwrap() - .write_cache(ansi_stdin_events.drain(..).collect()); - } - current_buffer.append(&mut buf.to_vec()); - let maybe_more = false; // read_from_stdin should (hopefully) always empty the STDIN buffer completely - let mut events = vec![]; - input_parser.parse( - &buf, - |input_event: InputEvent| { - events.push(input_event); - }, - maybe_more, - ); + if !ansi_stdin_events.is_empty() { + stdin_ansi_parser + .lock() + .unwrap() + .write_cache(ansi_stdin_events.drain(..).collect()); + } + current_buffer.append(&mut buf.to_vec()); + let maybe_more = false; // read_from_stdin should (hopefully) always empty the STDIN buffer completely + let mut events = vec![]; + input_parser.parse( + &buf, + |input_event: InputEvent| { + events.push(input_event); + }, + maybe_more, + ); - let event_count = events.len(); - for (i, input_event) in events.into_iter().enumerate() { - if holding_mouse && is_mouse_press_or_hold(&input_event) && i == event_count - 1 { - let mut poller = os_input.stdin_poller(); - loop { - if poller.ready() { - break; + let event_count = events.len(); + for (i, input_event) in events.into_iter().enumerate() { + if holding_mouse && is_mouse_press_or_hold(&input_event) && i == event_count - 1 + { + let mut poller = os_input.stdin_poller(); + loop { + if poller.ready() { + break; + } + send_input_instructions + .send(InputInstruction::KeyEvent( + input_event.clone(), + current_buffer.clone(), + )) + .unwrap(); + } } + + holding_mouse = is_mouse_press_or_hold(&input_event); + send_input_instructions .send(InputInstruction::KeyEvent( - input_event.clone(), - current_buffer.clone(), + input_event, + current_buffer.drain(..).collect(), )) .unwrap(); } - } - - holding_mouse = is_mouse_press_or_hold(&input_event); - - send_input_instructions - .send(InputInstruction::KeyEvent( - input_event, - current_buffer.drain(..).collect(), - )) - .unwrap(); + }, + Err(e) => { + if e == "Session ended" { + log::debug!("Switched sessions, signing this thread off..."); + } else { + log::error!("Failed to read from STDIN: {}", e); + } + let _ = send_input_instructions.send(InputInstruction::Exit); + break; + }, } } } diff --git a/zellij-client/src/unit/stdin_tests.rs b/zellij-client/src/unit/stdin_tests.rs index 6bdd489bc..31c7fe075 100644 --- a/zellij-client/src/unit/stdin_tests.rs +++ b/zellij-client/src/unit/stdin_tests.rs @@ -154,8 +154,9 @@ impl ClientOsApi for FakeClientOsApi { fn get_stdin_reader(&self) -> Box<dyn io::Read> { unimplemented!() } - fn read_from_stdin(&mut self) -> Vec<u8> { - self.stdin_buffer.drain(..).collect() + fn update_session_name(&mut self, new_session_name: String) {} + fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> { + Ok(self.stdin_buffer.drain(..).collect()) } fn box_clone(&self) -> Box<dyn ClientOsApi> { unimplemented!() |