//! Main input logic. use super::actions::Action; use super::keybinds::Keybinds; use crate::client::ClientInstruction; use crate::common::input::config::Config; use crate::common::thread_bus::{SenderWithContext, OPENCALLS}; use crate::errors::ContextType; use crate::os_input_output::ClientOsApi; use crate::server::ServerInstruction; use crate::CommandIsExecuting; use termion::input::{TermRead, TermReadEventsAndRaw}; use zellij_tile::data::{InputMode, Key, ModeInfo, Palette, PluginCapabilities}; /// 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, config: Config, command_is_executing: CommandIsExecuting, send_client_instructions: SenderWithContext, should_exit: bool, pasting: bool, } impl InputHandler { /// Returns a new [`InputHandler`] with the attributes specified as arguments. fn new( os_input: Box, command_is_executing: CommandIsExecuting, config: Config, send_client_instructions: SenderWithContext, ) -> Self { InputHandler { mode: InputMode::Normal, 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; } } termion::event::Event::Mouse(_) => { // Mouse events aren't implemented yet, // use a NoOp untill then. } }, Err(err) => panic!("Encountered read error: {:?}", err), } } } } fn handle_key(&mut self, key: &Key, raw_bytes: Vec) { 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 => { self.exit(); should_break = true; } Action::SwitchToMode(mode) => { self.mode = mode; self.os_input .send_to_server(ServerInstruction::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(ServerInstruction::Action(action)); self.command_is_executing .wait_until_input_thread_is_unblocked(); } _ => self .os_input .send_to_server(ServerInstruction::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) .unwrap(); } } /// Creates a [`Help`] struct indicating the current [`InputMode`] and its keybinds /// (as pairs of [`String`]s). // TODO this should probably be automatically generated in some way pub fn get_mode_info( mode: InputMode, palette: Palette, capabilities: PluginCapabilities, ) -> ModeInfo { let mut keybinds: Vec<(String, String)> = vec![]; match mode { InputMode::Normal | InputMode::Locked => {} InputMode::Resize => { keybinds.push(("←↓↑→".to_string(), "Resize".to_string())); } InputMode::Pane => { keybinds.push(("←↓↑→".to_string(), "Move focus".to_string())); keybinds.push(("p".to_string(), "Next".to_string())); keybinds.push(("n".to_string(), "New".to_string())); keybinds.push(("d".to_string(), "Down split".to_string())); keybinds.push(("r".to_string(), "Right split".to_string())); keybinds.push(("x".to_string(), "Close".to_string())); keybinds.push(("f".to_string(), "Fullscreen".to_string())); } InputMode::Tab => { keybinds.push(("←↓↑→".to_string(), "Move focus".to_string())); keybinds.push(("n".to_string(), "New".to_string())); keybinds.push(("x".to_string(), "Close".to_string())); keybinds.push(("r".to_string(), "Rename".to_string())); keybinds.push(("s".to_string(), "Sync".to_string())); } InputMode::Scroll => { keybinds.push(("↓↑".to_string(), "Scroll".to_string())); keybinds.push(("PgUp/PgDn".to_string(), "Scroll Page".to_string())); } InputMode::RenameTab => { keybinds.push(("Enter".to_string(), "when done".to_string())); } } ModeInfo { mode, keybinds, palette, capabilities, } } /// Entry point to the module. Instantiates an [`InputHandler`] and starts /// its [`InputHandler::handle_input()`] loop. pub fn input_loop( os_input: Box, config: Config, command_is_executing: CommandIsExecuting, send_client_instructions: SenderWithContext, ) { let _handler = InputHandler::new( os_input, command_is_executing, config, send_client_instructions, ) .handle_input(); } pub fn parse_keys(input_bytes: &[u8]) -> Vec { input_bytes.keys().flatten().map(cast_termion_key).collect() } // FIXME: This is an absolutely cursed function that should be destroyed as soon // as an alternative that doesn't touch zellij-tile can be developed... fn cast_termion_key(event: termion::event::Key) -> Key { match event { termion::event::Key::Backspace => Key::Backspace, termion::event::Key::Left => Key::Left, termion::event::Key::Right => Key::Right, termion::event::Key::Up => Key::Up, termion::event::Key::Down => Key::Down, termion::event::Key::Home => Key::Home, termion::event::Key::End => Key::End, termion::event::Key::PageUp => Key::PageUp, termion::event::Key::PageDown => Key::PageDown, termion::event::Key::BackTab => Key::BackTab, termion::event::Key::Delete => Key::Delete, termion::event::Key::Insert => Key::Insert, termion::event::Key::F(n) => Key::F(n), termion::event::Key::Char(c) => Key::Char(c), termion::event::Key::Alt(c) => Key::Alt(c), termion::event::Key::Ctrl(c) => Key::Ctrl(c), termion::event::Key::Null => Key::Null, termion::event::Key::Esc => Key::Esc, _ => { unimplemented!("Encountered an unknown key!") } } }