//! Main input logic. use super::actions::Action; use super::keybinds::get_default_keybinds; use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS}; use crate::errors::ContextType; use crate::os_input_output::OsApi; use crate::pty_bus::PtyInstruction; use crate::screen::ScreenInstruction; use crate::wasm_vm::PluginInstruction; use crate::CommandIsExecuting; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; use termion::input::TermReadEventsAndRaw; use super::keybinds::key_to_actions; /// 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, command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, send_plugin_instructions: SenderWithContext, send_app_instructions: SenderWithContext, } impl InputHandler { /// Returns a new [`InputHandler`] with the attributes specified as arguments. fn new( os_input: Box, command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, send_plugin_instructions: SenderWithContext, send_app_instructions: SenderWithContext, ) -> Self { InputHandler { mode: InputMode::Normal, os_input, command_is_executing, send_screen_instructions, send_pty_instructions, send_plugin_instructions, send_app_instructions, } } /// 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); self.send_pty_instructions.update(err_ctx); self.send_app_instructions.update(err_ctx); self.send_screen_instructions.update(err_ctx); if let Ok(keybinds) = get_default_keybinds() { 'input_loop: loop { //@@@ I think this should actually just iterate over stdin directly let stdin_buffer = self.os_input.read_from_stdin(); drop( self.send_plugin_instructions .send(PluginInstruction::GlobalInput(stdin_buffer.clone())), ); for key_result in stdin_buffer.events_and_raw() { match key_result { Ok((event, raw_bytes)) => match event { termion::event::Event::Key(key) => { // FIXME this explicit break is needed because the current test // framework relies on it to not create dead threads that loop // and eat up CPUs. Do not remove until the test framework has // been revised. Sorry about this (@categorille) if { let mut should_break = false; for action in key_to_actions(&key, raw_bytes, &self.mode, &keybinds) { should_break |= self.dispatch_action(action); } should_break } { break 'input_loop; } } termion::event::Event::Mouse(_) | termion::event::Event::Unsupported(_) => { unimplemented!("Mouse and unsupported events aren't supported!"); } }, Err(err) => panic!("Encountered read error: {:?}", err), } } } } else { //@@@ Error handling? self.exit(); } } /// 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::Write(val) => { self.send_screen_instructions .send(ScreenInstruction::ClearScroll) .unwrap(); self.send_screen_instructions .send(ScreenInstruction::WriteCharacter(val)) .unwrap(); } Action::Quit => { self.exit(); should_break = true; } Action::SwitchToMode(mode) => { self.mode = mode; update_state(&self.send_app_instructions, |_| AppState { input_mode: self.mode, }); self.send_screen_instructions .send(ScreenInstruction::Render) .unwrap(); } Action::Resize(direction) => { let screen_instr = match direction { super::actions::Direction::Left => ScreenInstruction::ResizeLeft, super::actions::Direction::Right => ScreenInstruction::ResizeRight, super::actions::Direction::Up => ScreenInstruction::ResizeUp, super::actions::Direction::Down => ScreenInstruction::ResizeDown, }; self.send_screen_instructions.send(screen_instr).unwrap(); } Action::SwitchFocus(_) => { self.send_screen_instructions .send(ScreenInstruction::MoveFocus) .unwrap(); } Action::MoveFocus(direction) => { let screen_instr = match direction { super::actions::Direction::Left => ScreenInstruction::MoveFocusLeft, super::actions::Direction::Right => ScreenInstruction::MoveFocusRight, super::actions::Direction::Up => ScreenInstruction::MoveFocusUp, super::actions::Direction::Down => ScreenInstruction::MoveFocusDown, }; self.send_screen_instructions.send(screen_instr).unwrap(); } Action::ScrollUp => { self.send_screen_instructions .send(ScreenInstruction::ScrollUp) .unwrap(); } Action::ScrollDown => { self.send_screen_instructions .send(ScreenInstruction::ScrollDown) .unwrap(); } Action::ToggleFocusFullscreen => { self.send_screen_instructions .send(ScreenInstruction::ToggleActiveTerminalFullscreen) .unwrap(); } Action::NewPane(direction) => { let pty_instr = match direction { Some(super::actions::Direction::Left) => { PtyInstruction::SpawnTerminalVertically(None) } Some(super::actions::Direction::Right) => { PtyInstruction::SpawnTerminalVertically(None) } Some(super::actions::Direction::Up) => { PtyInstruction::SpawnTerminalHorizontally(None) } Some(super::actions::Direction::Down) => { PtyInstruction::SpawnTerminalHorizontally(None) } // No direction specified - try to put it in the biggest available spot None => PtyInstruction::SpawnTerminal(None), }; self.command_is_executing.opening_new_pane(); self.send_pty_instructions.send(pty_instr).unwrap(); self.command_is_executing.wait_until_new_pane_is_opened(); } Action::CloseFocus => { self.command_is_executing.closing_pane(); self.send_screen_instructions .send(ScreenInstruction::CloseFocusedPane) .unwrap(); self.command_is_executing.wait_until_pane_is_closed(); } Action::NewTab => { self.command_is_executing.opening_new_pane(); self.send_pty_instructions .send(PtyInstruction::NewTab) .unwrap(); self.command_is_executing.wait_until_new_pane_is_opened(); } Action::GoToNextTab => { self.send_screen_instructions .send(ScreenInstruction::SwitchTabNext) .unwrap(); } Action::GoToPreviousTab => { self.send_screen_instructions .send(ScreenInstruction::SwitchTabPrev) .unwrap(); } Action::CloseTab => { self.command_is_executing.closing_pane(); self.send_screen_instructions .send(ScreenInstruction::CloseTab) .unwrap(); self.command_is_executing.wait_until_pane_is_closed(); } Action::GoToTab(i) => { self.send_screen_instructions .send(ScreenInstruction::GoToTab(i)) .unwrap(); } Action::NoOp => {} } 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_app_instructions .send(AppInstruction::Exit) .unwrap(); } } /// Describes the different input modes, which change the way that keystrokes will be interpreted. #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, EnumIter, Serialize, Deserialize)] pub enum InputMode { /// In `Normal` mode, input is always written to the terminal, except for one special input that /// triggers the switch to [`InputMode::Command`] mode. Normal, /// In `Command` mode, input is bound to actions (more precisely, sequences of actions). /// `Command` mode gives access to the other modes non-`InputMode::Normal` modes. /// etc. Command, /// `Resize` mode allows resizing the different existing panes. Resize, /// `Pane` mode allows creating and closing panes, as well as moving between them. Pane, /// `Tab` mode allows creating and closing tabs, as well as moving between them. Tab, /// `Scroll` mode allows scrolling up and down within a pane. Scroll, } /// Represents the contents of the help message that is printed in the status bar, /// which indicates the current [`InputMode`] and what the keybinds for that mode /// are. Related to the default `status-bar` plugin. #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Help { pub mode: InputMode, pub keybinds: Vec<(String, String)>, // => } impl Default for InputMode { fn default() -> InputMode { InputMode::Normal } } /// 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_help(mode: InputMode) -> Help { let mut keybinds: Vec<(String, String)> = vec![]; match mode { InputMode::Normal | InputMode::Command => { keybinds.push(("p".to_string(), "PANE".to_string())); keybinds.push(("t".to_string(), "TAB".to_string())); keybinds.push(("r".to_string(), "RESIZE".to_string())); keybinds.push(("s".to_string(), "SCROLL".to_string())); } 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(), "Split down".to_string())); keybinds.push(("r".to_string(), "Split right".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())); } InputMode::Scroll => { keybinds.push(("↓↑".to_string(), "Scroll".to_string())); } } keybinds.push(("ESC".to_string(), "BACK".to_string())); keybinds.push(("q".to_string(), "QUIT".to_string())); Help { mode, keybinds } } /// Entry point to the module. Instantiates an [`InputHandler`] and starts /// its [`InputHandler::handle_input()`] loop. pub fn input_loop( os_input: Box, command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, send_plugin_instructions: SenderWithContext, send_app_instructions: SenderWithContext, ) { let _handler = InputHandler::new( os_input, command_is_executing, send_screen_instructions, send_pty_instructions, send_plugin_instructions, send_app_instructions, ) .handle_input(); }