258 lines
10 KiB
Rust
258 lines
10 KiB
Rust
//! 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<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>,
|
|
) -> 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<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 => {
|
|
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<dyn ClientOsApi>,
|
|
config: Config,
|
|
command_is_executing: CommandIsExecuting,
|
|
send_client_instructions: SenderWithContext<ClientInstruction>,
|
|
) {
|
|
let _handler = InputHandler::new(
|
|
os_input,
|
|
command_is_executing,
|
|
config,
|
|
send_client_instructions,
|
|
)
|
|
.handle_input();
|
|
}
|
|
|
|
pub fn parse_keys(input_bytes: &[u8]) -> Vec<Key> {
|
|
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!")
|
|
}
|
|
}
|
|
}
|