summaryrefslogtreecommitdiffstats
path: root/zellij-client/src
diff options
context:
space:
mode:
authora-kenji <aks.kenji@protonmail.com>2021-05-27 13:28:59 +0200
committera-kenji <aks.kenji@protonmail.com>2021-05-27 15:49:50 +0200
commit81b026df249c72057ed9ae9e8b4140c2ff6db160 (patch)
treea0f28e0ebbf017b53acc41d2a2094886c4781acd /zellij-client/src
parent80fe803ffd49c0aa05c3883d04c0239e5dd5bae1 (diff)
parentf55805d653d86e4ac5c1386a980d929b09cb18e5 (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.rs33
-rw-r--r--zellij-client/src/input_handler.rs195
-rw-r--r--zellij-client/src/lib.rs298
-rw-r--r--zellij-client/src/os_input_output.rs190
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));
+ }
+ }