diff options
| author | Kyle Sutherland-Cash <kyle.sutherlandcash@gmail.com> | 2021-02-05 10:28:34 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-02-05 11:28:34 +0100 |
| commit | b6fb330da3820dcb1959c1ce749b59f0150c9e21 (patch) | |
| tree | bf6d83e64dfbc716d1a82f2b48401bdeb57e3453 /src/common/input/handler.rs | |
| parent | Merge pull request #157 from mosaic-org/restructure (diff) | |
| download | sl0ppij-b6fb330da3820dcb1959c1ce749b59f0150c9e21.tar.gz sl0ppij-b6fb330da3820dcb1959c1ce749b59f0150c9e21.tar.xz sl0ppij-b6fb330da3820dcb1959c1ce749b59f0150c9e21.zip | |
refactor(input): hotkeys (#132)
* Initial definitions and thoughts for hotkeys
* Actually document InputKey properly
* Add a to string function for input keys
* Define keybinds and actions; restructure
* Implement hash and start on defining key bindings
* Derive Serialize for input keys
* Store the key strings as tuples for two-way mapping
* Some string to key functions
* Use termion's Key definition and implement action dispatch
* Fix some borrow-checker errors
* Missing keybind and command mode switching
* Fix incorrect handling of spawn terminal command
* fix(plugins): work with new input - tests not passing
* fix(infra): stabilize tests and properly close pty sessions
* style(fmt): rustfmt
Co-authored-by: Brooks J Rady <b.j.rady@gmail.com>
Co-authored-by: Aram Drevekenin <aram@poor.dev>
Diffstat (limited to 'src/common/input/handler.rs')
| -rw-r--r-- | src/common/input/handler.rs | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs new file mode 100644 index 00000000..b10e9df3 --- /dev/null +++ b/src/common/input/handler.rs @@ -0,0 +1,291 @@ +use super::actions::Action; +use super::keybinds::get_default_keybinds; +use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS}; +/// Module for handling input +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 strum_macros::EnumIter; +use termion::input::TermReadEventsAndRaw; + +use super::keybinds::key_to_action; + +struct InputHandler { + mode: InputMode, + os_input: Box<dyn OsApi>, + command_is_executing: CommandIsExecuting, + send_screen_instructions: SenderWithContext<ScreenInstruction>, + send_pty_instructions: SenderWithContext<PtyInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + send_app_instructions: SenderWithContext<AppInstruction>, +} + +impl InputHandler { + fn new( + os_input: Box<dyn OsApi>, + command_is_executing: CommandIsExecuting, + send_screen_instructions: SenderWithContext<ScreenInstruction>, + send_pty_instructions: SenderWithContext<PtyInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + send_app_instructions: SenderWithContext<AppInstruction>, + ) -> Self { + InputHandler { + mode: InputMode::Normal, + os_input, + command_is_executing, + send_screen_instructions, + send_pty_instructions, + send_plugin_instructions, + send_app_instructions, + } + } + + /// Main event loop + fn get_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 { + let entry_mode = self.mode; + //@@@ 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) => { + let should_break = self.dispatch_action(key_to_action( + &key, raw_bytes, &self.mode, &keybinds, + )); + //@@@ This is a hack until we dispatch more than one action per key stroke + if entry_mode == InputMode::Command + && self.mode == InputMode::Command + { + self.mode = InputMode::Normal; + update_state(&self.send_app_instructions, |_| AppState { + input_mode: self.mode, + }); + } + if 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(); + } + } + + fn dispatch_action(&mut self, action: Action) -> bool { + let mut interrupt_loop = 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(); + interrupt_loop = 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(); + } + } + + interrupt_loop + } + + /// Routine to be called when the input handler exits (at the moment this is the + /// same as quitting mosaic) + fn exit(&mut self) { + self.send_app_instructions + .send(AppInstruction::Exit) + .unwrap(); + } +} + +/// Dictates whether we're in command mode, persistent command mode, normal mode or exiting: +/// - Normal mode either writes characters to the terminal, or switches to command mode +/// using a particular key control +/// - Command mode intercepts characters to control mosaic itself, before switching immediately +/// back to normal mode +/// - Persistent command mode is the same as command mode, but doesn't return automatically to +/// normal mode +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, EnumIter)] +pub enum InputMode { + Normal, + Command, + CommandPersistent, + Exiting, +} + +// FIXME: This should be auto-generated from the soon-to-be-added `get_default_keybinds` +pub fn get_help(mode: &InputMode) -> Vec<String> { + let command_help = vec![ + "<n/b/z> Split".into(), + "<j/k/h/l> Resize".into(), + "<p> Focus Next".into(), + "<x> Close Pane".into(), + "<q> Quit".into(), + "<PgUp/PgDown> Scroll".into(), + "<1> New Tab".into(), + "<2/3> Move Tab".into(), + "<4> Close Tab".into(), + ]; + match mode { + InputMode::Normal => vec!["<Ctrl-g> Command Mode".into()], + InputMode::Command => [ + vec![ + "<Ctrl-g> Persistent Mode".into(), + "<ESC> Normal Mode".into(), + ], + command_help, + ] + .concat(), + InputMode::CommandPersistent => { + [vec!["<ESC/Ctrl-g> Normal Mode".into()], command_help].concat() + } + InputMode::Exiting => vec!["Bye from Mosaic!".into()], + } +} + +/// Entry point to the module that instantiates a new InputHandler and calls its +/// reading loop +pub fn input_loop( + os_input: Box<dyn OsApi>, + command_is_executing: CommandIsExecuting, + send_screen_instructions: SenderWithContext<ScreenInstruction>, + send_pty_instructions: SenderWithContext<PtyInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + send_app_instructions: SenderWithContext<AppInstruction>, +) { + let _handler = InputHandler::new( + os_input, + command_is_executing, + send_screen_instructions, + send_pty_instructions, + send_plugin_instructions, + send_app_instructions, + ) + .get_input(); +} |
