* 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>
291 lines
12 KiB
Rust
291 lines
12 KiB
Rust
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();
|
|
}
|