diff options
author | a-kenji <aks.kenji@protonmail.com> | 2021-05-27 13:28:59 +0200 |
---|---|---|
committer | a-kenji <aks.kenji@protonmail.com> | 2021-05-27 15:49:50 +0200 |
commit | 81b026df249c72057ed9ae9e8b4140c2ff6db160 (patch) | |
tree | a0f28e0ebbf017b53acc41d2a2094886c4781acd /zellij-client/src | |
parent | 80fe803ffd49c0aa05c3883d04c0239e5dd5bae1 (diff) | |
parent | f55805d653d86e4ac5c1386a980d929b09cb18e5 (diff) |
Merge branch 'main' of https://github.com/zellij-org/zellij into default-mode-368
* If starting in the locked mode after the merge,
the locked mode seems to need 2 actions to go to
the normal mode - after that everything works
as expected.
- This is not intended.
Diffstat (limited to 'zellij-client/src')
-rw-r--r-- | zellij-client/src/command_is_executing.rs | 33 | ||||
-rw-r--r-- | zellij-client/src/input_handler.rs | 195 | ||||
-rw-r--r-- | zellij-client/src/lib.rs | 298 | ||||
-rw-r--r-- | zellij-client/src/os_input_output.rs | 190 |
4 files changed, 716 insertions, 0 deletions
diff --git a/zellij-client/src/command_is_executing.rs b/zellij-client/src/command_is_executing.rs new file mode 100644 index 000000000..3d672d9a2 --- /dev/null +++ b/zellij-client/src/command_is_executing.rs @@ -0,0 +1,33 @@ +#![allow(clippy::mutex_atomic)] +use std::sync::{Arc, Condvar, Mutex}; + +#[derive(Clone)] +pub(crate) struct CommandIsExecuting { + input_thread: Arc<(Mutex<bool>, Condvar)>, +} + +impl CommandIsExecuting { + pub fn new() -> Self { + CommandIsExecuting { + input_thread: Arc::new((Mutex::new(false), Condvar::new())), + } + } + pub fn blocking_input_thread(&mut self) { + let (lock, _cvar) = &*self.input_thread; + let mut input_thread = lock.lock().unwrap(); + *input_thread = true; + } + pub fn unblock_input_thread(&mut self) { + let (lock, cvar) = &*self.input_thread; + let mut input_thread = lock.lock().unwrap(); + *input_thread = false; + cvar.notify_all(); + } + pub fn wait_until_input_thread_is_unblocked(&self) { + let (lock, cvar) = &*self.input_thread; + let mut input_thread = lock.lock().unwrap(); + while *input_thread { + input_thread = cvar.wait(input_thread).unwrap(); + } + } +} diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs new file mode 100644 index 000000000..65181e512 --- /dev/null +++ b/zellij-client/src/input_handler.rs @@ -0,0 +1,195 @@ +//! Main input logic. + +use zellij_utils::{termion, zellij_tile}; + +use crate::{os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting}; +use zellij_utils::{ + channels::{SenderWithContext, OPENCALLS}, + errors::ContextType, + input::{actions::Action, cast_termion_key, config::Config, keybinds::Keybinds}, + ipc::{ClientToServerMsg, ExitReason}, +}; + +use termion::input::TermReadEventsAndRaw; +use zellij_tile::data::{InputMode, Key}; + +/// Handles the dispatching of [`Action`]s according to the current +/// [`InputMode`], and keep tracks of the current [`InputMode`]. +struct InputHandler { + /// The current input mode + mode: InputMode, + os_input: Box<dyn ClientOsApi>, + config: Config, + command_is_executing: CommandIsExecuting, + send_client_instructions: SenderWithContext<ClientInstruction>, + should_exit: bool, + pasting: bool, +} + +impl InputHandler { + /// Returns a new [`InputHandler`] with the attributes specified as arguments. + fn new( + os_input: Box<dyn ClientOsApi>, + command_is_executing: CommandIsExecuting, + config: Config, + send_client_instructions: SenderWithContext<ClientInstruction>, + mode: InputMode, + ) -> Self { + InputHandler { + mode, + os_input, + config, + command_is_executing, + send_client_instructions, + should_exit: false, + pasting: false, + } + } + + /// Main input event loop. Interprets the terminal [`Event`](termion::event::Event)s + /// as [`Action`]s according to the current [`InputMode`], and dispatches those actions. + fn handle_input(&mut self) { + let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); + err_ctx.add_call(ContextType::StdinHandler); + let alt_left_bracket = vec![27, 91]; + let bracketed_paste_start = vec![27, 91, 50, 48, 48, 126]; // \u{1b}[200~ + let bracketed_paste_end = vec![27, 91, 50, 48, 49, 126]; // \u{1b}[201 + loop { + if self.should_exit { + break; + } + let stdin_buffer = self.os_input.read_from_stdin(); + for key_result in stdin_buffer.events_and_raw() { + match key_result { + Ok((event, raw_bytes)) => match event { + termion::event::Event::Key(key) => { + let key = cast_termion_key(key); + self.handle_key(&key, raw_bytes); + } + termion::event::Event::Unsupported(unsupported_key) => { + // we have to do this because of a bug in termion + // this should be a key event and not an unsupported event + if unsupported_key == alt_left_bracket { + let key = Key::Alt('['); + self.handle_key(&key, raw_bytes); + } else if unsupported_key == bracketed_paste_start { + self.pasting = true; + } else if unsupported_key == bracketed_paste_end { + self.pasting = false; + } else { + // this is a hack because termion doesn't recognize certain keys + // in this case we just forward it to the terminal + self.handle_unknown_key(raw_bytes); + } + } + termion::event::Event::Mouse(_) => { + // Mouse events aren't implemented yet, + // use a NoOp untill then. + } + }, + Err(err) => panic!("Encountered read error: {:?}", err), + } + } + } + } + fn handle_unknown_key(&mut self, raw_bytes: Vec<u8>) { + if self.mode == InputMode::Normal || self.mode == InputMode::Locked { + let action = Action::Write(raw_bytes); + self.dispatch_action(action); + } + } + fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>) { + let keybinds = &self.config.keybinds; + if self.pasting { + // we're inside a paste block, if we're in a mode that allows sending text to the + // terminal, send all text directly without interpreting it + // otherwise, just discard the input + if self.mode == InputMode::Normal || self.mode == InputMode::Locked { + let action = Action::Write(raw_bytes); + self.dispatch_action(action); + } + } else { + for action in Keybinds::key_to_actions(&key, raw_bytes, &self.mode, keybinds) { + let should_exit = self.dispatch_action(action); + if should_exit { + self.should_exit = true; + } + } + } + } + + /// Dispatches an [`Action`]. + /// + /// This function's body dictates what each [`Action`] actually does when + /// dispatched. + /// + /// # Return value + /// Currently, this function returns a boolean that indicates whether + /// [`Self::handle_input()`] should break after this action is dispatched. + /// This is a temporary measure that is only necessary due to the way that the + /// framework works, and shouldn't be necessary anymore once the test framework + /// is revised. See [issue#183](https://github.com/zellij-org/zellij/issues/183). + fn dispatch_action(&mut self, action: Action) -> bool { + let mut should_break = false; + + match action { + Action::Quit | Action::Detach => { + self.os_input + .send_to_server(ClientToServerMsg::Action(action)); + self.exit(); + should_break = true; + } + Action::SwitchToMode(mode) => { + self.mode = mode; + self.os_input + .send_to_server(ClientToServerMsg::Action(action)); + } + Action::CloseFocus + | Action::NewPane(_) + | Action::NewTab + | Action::GoToNextTab + | Action::GoToPreviousTab + | Action::CloseTab + | Action::GoToTab(_) + | Action::MoveFocusOrTab(_) => { + self.command_is_executing.blocking_input_thread(); + self.os_input + .send_to_server(ClientToServerMsg::Action(action)); + self.command_is_executing + .wait_until_input_thread_is_unblocked(); + } + _ => self + .os_input + .send_to_server(ClientToServerMsg::Action(action)), + } + + should_break + } + + /// Routine to be called when the input handler exits (at the moment this is the + /// same as quitting Zellij). + fn exit(&mut self) { + self.send_client_instructions + .send(ClientInstruction::Exit(ExitReason::Normal)) + .unwrap(); + } +} + +/// Entry point to the module. Instantiates an [`InputHandler`] and starts +/// its [`InputHandler::handle_input()`] loop. +pub(crate) fn input_loop( + os_input: Box<dyn ClientOsApi>, + config: Config, + command_is_executing: CommandIsExecuting, + send_client_instructions: SenderWithContext<ClientInstruction>, + default_mode: InputMode, +) { + let _handler = InputHandler::new( + os_input, + command_is_executing, + config, + send_client_instructions, + default_mode, + ) + .handle_input(); +} diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs new file mode 100644 index 000000000..d854b6d38 --- /dev/null +++ b/zellij-client/src/lib.rs @@ -0,0 +1,298 @@ +pub mod os_input_output; + +mod command_is_executing; +mod input_handler; + +use std::env::current_exe; +use std::io::{self, Write}; +use std::path::Path; +use std::process::Command; +use std::sync::mpsc; +use std::thread; + +use crate::{ + command_is_executing::CommandIsExecuting, input_handler::input_loop, + os_input_output::ClientOsApi, +}; +use zellij_utils::cli::CliArgs; +use zellij_utils::{ + channels::{SenderType, SenderWithContext, SyncChannelWithContext}, + consts::{SESSION_NAME, ZELLIJ_IPC_PIPE}, + errors::{ClientContext, ContextType, ErrorInstruction}, + 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(String), + UnblockInputThread, + Exit(ExitReason), +} + +impl From<ServerToClientMsg> for ClientInstruction { + fn from(instruction: ServerToClientMsg) -> Self { + match instruction { + ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e), + ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer), + ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread, + } + } +} + +impl From<&ClientInstruction> for ClientContext { + fn from(client_instruction: &ClientInstruction) -> Self { + match *client_instruction { + ClientInstruction::Exit(_) => ClientContext::Exit, + ClientInstruction::Error(_) => ClientContext::Error, + ClientInstruction::Render(_) => ClientContext::Render, + ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread, + } + } +} + +impl ErrorInstruction for ClientInstruction { + fn error(err: String) -> Self { + ClientInstruction::Error(err) + } +} + +fn spawn_server(socket_path: &Path) -> io::Result<()> { + let status = Command::new(current_exe()?) + .arg("--server") + .arg(socket_path) + .status()?; + if status.success() { + Ok(()) + } else { + let msg = "Process returned non-zero exit code"; + let err_msg = match status.code() { + Some(c) => format!("{}: {}", msg, c), + None => msg.to_string(), + }; + Err(io::Error::new(io::ErrorKind::Other, err_msg)) + } +} + +#[derive(Debug, Clone)] +pub enum ClientInfo { + Attach(String, bool), + New(String), +} + +pub fn start_client( + mut os_input: Box<dyn ClientOsApi>, + 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"; + os_input.unset_raw_mode(0); + let palette = os_input.load_palette(); + let _ = os_input + .get_stdout_writer() + .write(take_snapshot.as_bytes()) + .unwrap(); + let _ = os_input + .get_stdout_writer() + .write(clear_client_terminal_attributes.as_bytes()) + .unwrap(); + std::env::set_var(&"ZELLIJ", "0"); + + 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.clone()), + ) + } + }; + #[cfg(any(feature = "test", test))] + let first_msg = { + let _ = SESSION_NAME.set("".into()); + ClientToServerMsg::NewClient(client_attributes, Box::new(opts), Box::new(config_options.clone())) + }; + + os_input.connect_to_server(&*ZELLIJ_IPC_PIPE); + 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() + .write(bracketed_paste.as_bytes()) + .unwrap(); + + let (send_client_instructions, receive_client_instructions): SyncChannelWithContext< + ClientInstruction, + > = mpsc::sync_channel(50); + let send_client_instructions = + SenderWithContext::new(SenderType::SyncSender(send_client_instructions)); + + #[cfg(not(any(feature = "test", test)))] + std::panic::set_hook({ + use zellij_utils::errors::handle_panic; + let send_client_instructions = send_client_instructions.clone(); + Box::new(move |info| { + handle_panic(info, &send_client_instructions); + }) + }); + + let _stdin_thread = thread::Builder::new() + .name("stdin_handler".to_string()) + .spawn({ + let send_client_instructions = send_client_instructions.clone(); + let command_is_executing = command_is_executing.clone(); + let os_input = os_input.clone(); + let default_mode = config_options.default_mode.unwrap_or_default(); + move || { + input_loop( + os_input, + config, + command_is_executing, + send_client_instructions, + default_mode, + ) + } + }); + + let _signal_thread = thread::Builder::new() + .name("signal_listener".to_string()) + .spawn({ + let os_input = os_input.clone(); + let send_client_instructions = send_client_instructions.clone(); + move || { + os_input.handle_signals( + Box::new({ + let os_api = os_input.clone(); + move || { + os_api.send_to_server(ClientToServerMsg::TerminalResize( + os_api.get_terminal_size_using_fd(0), + )); + } + }), + Box::new({ + let send_client_instructions = send_client_instructions.clone(); + move || { + send_client_instructions + .send(ClientInstruction::Exit(ExitReason::Normal)) + .unwrap() + } + }), + ); + } + }) + .unwrap(); + + let router_thread = thread::Builder::new() + .name("router".to_string()) + .spawn({ + let os_input = os_input.clone(); + let mut should_break = false; + move || loop { + let (instruction, err_ctx) = os_input.recv_from_server(); + err_ctx.update_thread_ctx(); + if let ServerToClientMsg::Exit(_) = instruction { + should_break = true; + } + send_client_instructions.send(instruction.into()).unwrap(); + if should_break { + break; + } + } + }) + .unwrap(); + + let handle_error = |backtrace: String| { + os_input.unset_raw_mode(0); + let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); + let restore_snapshot = "\u{1b}[?1049l"; + let error = format!( + "{}\n{}{}", + goto_start_of_last_line, restore_snapshot, backtrace + ); + let _ = os_input + .get_stdout_writer() + .write(error.as_bytes()) + .unwrap(); + std::process::exit(1); + }; + + let exit_msg: String; + + loop { + let (client_instruction, mut err_ctx) = receive_client_instructions + .recv() + .expect("failed to receive app instruction on channel"); + + err_ctx.add_call(ContextType::Client((&client_instruction).into())); + match client_instruction { + 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::Error(backtrace) => { + let _ = os_input.send_to_server(ClientToServerMsg::Action(Action::Quit)); + handle_error(backtrace); + } + ClientInstruction::Render(output) => { + let mut stdout = os_input.get_stdout_writer(); + stdout + .write_all(&output.as_bytes()) + .expect("cannot write to stdout"); + stdout.flush().expect("could not flush"); + } + ClientInstruction::UnblockInputThread => { + command_is_executing.unblock_input_thread(); + } + } + } + + 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.unset_raw_mode(0); + let mut stdout = os_input.get_stdout_writer(); + let _ = stdout.write(goodbye_message.as_bytes()).unwrap(); + stdout.flush().unwrap(); +} diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs new file mode 100644 index 000000000..9d8617f5f --- /dev/null +++ b/zellij-client/src/os_input_output.rs @@ -0,0 +1,190 @@ +use zellij_utils::{interprocess, libc, nix, signal_hook, zellij_tile}; + +use interprocess::local_socket::LocalSocketStream; +use nix::pty::Winsize; +use nix::sys::termios; +use signal_hook::{consts::signal::*, iterator::Signals}; +use std::io; +use std::io::prelude::*; +use std::os::unix::io::RawFd; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use zellij_tile::data::{Palette, PaletteColor}; +use zellij_utils::{ + errors::ErrorContext, + ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg}, + pane_size::PositionAndSize, + shared::default_palette, +}; + +fn into_raw_mode(pid: RawFd) { + let mut tio = termios::tcgetattr(pid).expect("could not get terminal attribute"); + termios::cfmakeraw(&mut tio); + match termios::tcsetattr(pid, termios::SetArg::TCSANOW, &tio) { + Ok(_) => {} + Err(e) => panic!("error {:?}", e), + }; +} + +fn unset_raw_mode(pid: RawFd, orig_termios: termios::Termios) { + match termios::tcsetattr(pid, termios::SetArg::TCSANOW, &orig_termios) { + Ok(_) => {} + Err(e) => panic!("error {:?}", e), + }; +} + +pub(crate) fn get_terminal_size_using_fd(fd: RawFd) -> PositionAndSize { + // TODO: do this with the nix ioctl + use libc::ioctl; + use libc::TIOCGWINSZ; + + let mut winsize = Winsize { + ws_row: 0, + ws_col: 0, + ws_xpixel: 0, + ws_ypixel: 0, + }; + + unsafe { ioctl(fd, TIOCGWINSZ, &mut winsize) }; + PositionAndSize::from(winsize) +} + +#[derive(Clone)] +pub struct ClientOsInputOutput { + orig_termios: Arc<Mutex<termios::Termios>>, + send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>, + receive_instructions_from_server: Arc<Mutex<Option<IpcReceiverWithContext<ServerToClientMsg>>>>, +} + +/// The `ClientOsApi` trait represents an abstract interface to the features of an operating system that +/// Zellij client requires. +pub trait ClientOsApi: Send + Sync { + /// Returns the size of the terminal associated to file descriptor `fd`. + fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize; + /// Set the terminal associated to file descriptor `fd` to + /// [raw mode](https://en.wikipedia.org/wiki/Terminal_mode). + fn set_raw_mode(&mut self, fd: RawFd); + /// Set the terminal associated to file descriptor `fd` to + /// [cooked mode](https://en.wikipedia.org/wiki/Terminal_mode). + fn unset_raw_mode(&self, fd: RawFd); + /// Returns the writer that allows writing to standard output. + fn get_stdout_writer(&self) -> Box<dyn io::Write>; + /// Returns the raw contents of standard input. + fn read_from_stdin(&self) -> Vec<u8>; + /// Returns a [`Box`] pointer to this [`ClientOsApi`] struct. + fn box_clone(&self) -> Box<dyn ClientOsApi>; + /// Sends a message to the server. + fn send_to_server(&self, msg: ClientToServerMsg); + /// Receives a message on client-side IPC channel + // This should be called from the client-side router thread only. + fn recv_from_server(&self) -> (ServerToClientMsg, ErrorContext); + fn handle_signals(&self, sigwinch_cb: Box<dyn Fn()>, quit_cb: Box<dyn Fn()>); + /// Establish a connection with the server socket. + fn connect_to_server(&self, path: &Path); + fn load_palette(&self) -> Palette; +} + +impl ClientOsApi for ClientOsInputOutput { + fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize { + get_terminal_size_using_fd(fd) + } + fn set_raw_mode(&mut self, fd: RawFd) { + into_raw_mode(fd); + } + fn unset_raw_mode(&self, fd: RawFd) { + let orig_termios = self.orig_termios.lock().unwrap(); + unset_raw_mode(fd, orig_termios.clone()); + } + fn box_clone(&self) -> Box<dyn ClientOsApi> { + Box::new((*self).clone()) + } + fn read_from_stdin(&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 get_stdout_writer(&self) -> Box<dyn io::Write> { + let stdout = ::std::io::stdout(); + Box::new(stdout) + } + fn send_to_server(&self, msg: ClientToServerMsg) { + self.send_instructions_to_server + .lock() + .unwrap() + .as_mut() + .unwrap() + .send(msg); + } + fn recv_from_server(&self) -> (ServerToClientMsg, ErrorContext) { + self.receive_instructions_from_server + .lock() + .unwrap() + .as_mut() + .unwrap() + .recv() + } + fn handle_signals(&self, sigwinch_cb: Box<dyn Fn()>, quit_cb: Box<dyn Fn()>) { + let mut signals = Signals::new(&[SIGWINCH, SIGTERM, SIGINT, SIGQUIT, SIGHUP]).unwrap(); + for signal in signals.forever() { + match signal { + SIGWINCH => { + sigwinch_cb(); + } + SIGTERM | SIGINT | SIGQUIT | SIGHUP => { + quit_cb(); + break; + } + _ => unreachable!(), + } + } + } + fn connect_to_server(&self, path: &Path) { + let socket; + loop { + match LocalSocketStream::connect(path) { + Ok(sock) => { + socket = sock; + break; + } + Err(_) => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } |