summaryrefslogtreecommitdiffstats
path: root/zellij-client
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2023-08-24 13:36:24 +0200
committerGitHub <noreply@github.com>2023-08-24 13:36:24 +0200
commitbc628abc1266cdc0dbce4f19a89727527a3e39a8 (patch)
tree6a0fcac6ec35c71a237bca0128a57a5b40afb0e3 /zellij-client
parentbf3c072d6dd68da0abd838e95e6004091a4cd331 (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.rs3
-rw-r--r--zellij-client/src/lib.rs116
-rw-r--r--zellij-client/src/os_input_output.rs59
-rw-r--r--zellij-client/src/stdin_handler.rs117
-rw-r--r--zellij-client/src/unit/stdin_tests.rs5
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!()