use crate::{
os_input_output::ClientOsApi, stdin_ansi_parser::AnsiStdinInstruction, ClientId,
ClientInstruction, CommandIsExecuting, InputInstruction,
};
use zellij_utils::{
channels::{Receiver, SenderWithContext, OPENCALLS},
data::{InputMode, KeyWithModifier},
errors::{ContextType, ErrorContext, FatalError},
input::{
actions::Action,
cast_termwiz_key,
config::Config,
mouse::{MouseEvent, MouseEventType},
options::Options,
},
ipc::{ClientToServerMsg, ExitReason},
position::Position,
vendored::termwiz::input::{
InputEvent, Modifiers, MouseButtons, MouseEvent as TermwizMouseEvent,
},
};
struct InputHandler {
mode: InputMode,
os_input: Box<dyn ClientOsApi>,
config: Config,
options: Options,
command_is_executing: CommandIsExecuting,
send_client_instructions: SenderWithContext<ClientInstruction>,
should_exit: bool,
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
mouse_old_event: MouseEvent,
mouse_mode_active: bool,
}
fn termwiz_mouse_convert(original_event: &mut MouseEvent, event: &TermwizMouseEvent) {
let button_bits = &event.mouse_buttons;
original_event.left = button_bits.contains(MouseButtons::LEFT);
original_event.right = button_bits.contains(MouseButtons::RIGHT);
original_event.middle = button_bits.contains(MouseButtons::MIDDLE);
original_event.wheel_up = button_bits.contains(MouseButtons::VERT_WHEEL)
&& button_bits.contains(MouseButtons::WHEEL_POSITIVE);
original_event.wheel_down = button_bits.contains(MouseButtons::VERT_WHEEL)
&& !button_bits.contains(MouseButtons::WHEEL_POSITIVE);
let mods = &event.modifiers;
original_event.shift = mods.contains(Modifiers::SHIFT);
original_event.alt = mods.contains(Modifiers::ALT);
original_event.ctrl = mods.contains(Modifiers::CTRL);
}
pub fn from_termwiz(old_event: &mut MouseEvent, event: TermwizMouseEvent) -> MouseEvent {
let mut new_event = MouseEvent::new();
termwiz_mouse_convert(&mut new_event, &event);
new_event.position = Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1));
if (new_event.left && !old_event.left)
|| (new_event.right && !old_event.right)
|| (new_event.middle && !old_event.middle)
|| new_event.wheel_up
|| new_event.wheel_down
{
new_event.event_type = MouseEventType::Press;
*old_event = new_event;
} else if event.mouse_buttons.is_empty()
&& !old_event.left
&& !old_event.right
&& !old_event.middle
{
new_event.event_type = MouseEventType::Motion;
*old_event = new_event;
} else if event.mouse_buttons.is_empty()
&& (old_event.left || old_event.right || old_event.middle)
{
if old_event.left {
old_event.left = false;
new_event.left = true;
}
if old_event.right {
old_event.right = false;
new_event.right = true;
}
if old_event.middle {
old_event.middle = false;
new_event.middle = true;
}
new_event.event_type = MouseEventType::Release;
} else {
new_event.event_type = MouseEventType::Motion;
*old_event = new_event;
}
new_event
}
impl InputHandler {
fn new(
os_input: Box<dyn ClientOsApi>,
command_is_executing: CommandIsExecuting,
config: Config,
options: Options,
send_client_instructions: SenderWithContext<ClientInstruction>,
mode: InputMode, receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
) -> Self {
InputHandler {
mode,
os_input,
config,
options,
command_is_executing,
send_client_instructions,
should_exit: false,
receive_input_instructions,
mouse_old_event: MouseEvent::new(),
mouse_mode_active: false,
}
}
fn handle_input(&mut self) {
let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
err_ctx.add_call(ContextType::StdinHandler);
let bracketed_paste_start = vec![27, 91, 50, 48, 48, 126]; let bracketed_paste_end = vec![27, 91, 50, 48, 49, 126]; if self.options.mouse_mode.unwrap_or(true) {
self.os_input.enable_mouse().non_fatal();
self.mouse_mode_active = true;
}
loop {
if self.should_exit {
break;
}
match self.receive_input_instructions.recv() {
Ok((InputInstruction::KeyEvent(input_event, raw_bytes), _error_context)) => {
match input_event {
InputEvent::Key(key_event) => {
let key = cast_termwiz_key(
key_event,
&raw_bytes,
Some((&self.config.keybinds, &self.mode)),
);
self.handle_key(&key, raw_bytes, false);
},
InputEvent::Mouse(mouse_event) => {
let mouse_event = from_termwiz(&mut self.mouse_old_event, mouse_event);
self.handle_mouse_event(&mouse_event);
},
InputEvent::Paste(pasted_text) => {
if self.mode == InputMode::Normal || self.mode == InputMode::Locked {
self.dispatch_action(
Action::Write {
key_with_modifier: None,
bytes: bracketed_paste_start.clone(),
is_kitty_keyboard_protocol: false,
},
None,
);
self.dispatch_action(
Action::Write {
key_with_modifier: None,
bytes: pasted_text.as_bytes().to_vec(),
is_kitty_keyboard_protocol: false,
},
None,
);
self.dispatch_action(
Action::Write {
key_with_modifier: None,
bytes: bracketed_paste_end.clone(),
is_kitty_keyboard_protocol: false,
},
None,
);
}
if self.mode == InputMode::EnterSearch {
self.dispatch_action(
Action::SearchInput {
input: pasted_text.as_bytes().to_vec(),
},
None,
);
}
if self.mode == InputMode::RenameTab {
self.dispatch_action(
Action::TabNameInput {
input: pasted_text.as_bytes().to_vec(),
},
None,
);
}
if self.mode == InputMode::RenamePane {
self.dispatch_action(
Action::PaneNameInput {
input: pasted_text.as_bytes().to_vec(),
},
None,
);
}
},
_ => {},
}
},
Ok((
InputInstruction::KeyWithModifierEvent(key_with_modifier, raw_bytes, is_kitty),
_error_context,
)) => {
self.handle_key(&key_with_modifier, raw_bytes, is_kitty);
},
Ok((InputInstruction::MouseEvent(mouse_event), _error_context)) => {
self.handle_mouse_event(&mouse_event);
},
Ok((
InputInstruction::AnsiStdinInstructions(ansi_stdin_instructions),
_error_context,
)) => {
for ansi_instruction in ansi_stdin_instructions {
self.handle_stdin_ansi_instruction(ansi_instruction);
}
},
Ok((InputInstruction::DesktopNotificationResponse(raw_bytes), _error_context)) => {
self.os_input
.send_to_server(ClientToServerMsg::DesktopNotificationResponse {
raw_bytes,
});
},
Ok((InputInstruction::StartedParsing, _error_context)) => {
self.send_client_instructions
.send(ClientInstruction::StartedParsingStdinQuery)
.unwrap();
},
Ok((InputInstruction::DoneParsing, _error_context)) => {
self.send_client_instructions
.send(ClientInstruction::DoneParsingStdinQuery)
.unwrap();
},
Ok((InputInstruction::Exit, _error_context)) => {
self.should_exit = true;
},
Err(err) => panic!("Encountered read error: {:?}", err),
}
}
}
fn handle_key(
&mut self,
key: &KeyWithModifier,
raw_bytes: Vec<u8>,
is_kitty_keyboard_protocol: bool,
) {
self.os_input.send_to_server(ClientToServerMsg::Key {
key: key.clone(),
raw_bytes,
is_kitty_keyboard_protocol,
});
}
fn handle_stdin_ansi_instruction(&mut self, ansi_stdin_instructions: AnsiStdinInstruction) {
match ansi_stdin_instructions {
AnsiStdinInstruction::PixelDimensions(pixel_dimensions) => {
self.os_input
.send_to_server(ClientToServerMsg::TerminalPixelDimensions {
pixel_dimensions,
});
},
AnsiStdinInstruction::BackgroundColor(background_color_instruction) => {
self.os_input
.send_to_server(ClientToServerMsg::BackgroundColor {
color: background_color_instruction,
});
},
AnsiStdinInstruction::ForegroundColor(foreground_color_instruction) => {
self.os_input
.send_to_server(ClientToServerMsg::ForegroundColor {
color: foreground_color_instruction,
});
},
AnsiStdinInstruction::ColorRegisters(color_registers) => {
let color_registers: Vec<_> = color_registers
.into_iter()
.map(|(index, color)| zellij_utils::ipc::ColorRegister { index, color })
.collect();
self.os_input
.send_to_server(ClientToServerMsg::ColorRegisters { color_registers });
},
AnsiStdinInstruction::SynchronizedOutput(enabled) => {
self.send_client_instructions
.send(ClientInstruction::SetSynchronizedOutput(enabled))
.unwrap();
},
}
}
fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) {
self.dispatch_action(
Action::MouseEvent {
event: *mouse_event,
},
None,
);
}
fn dispatch_action(&mut self, action: Action, client_id: Option<ClientId>) -> bool {
let mut should_break = false;
match action {
Action::NoOp => {},
Action::Quit => {
self.os_input.send_to_server(ClientToServerMsg::Action {
action,
terminal_id: None,
client_id,
is_cli_client: false,
});
self.exit(ExitReason::Normal);
should_break = true;
},
Action::Detach => {
self.os_input.send_to_server(ClientToServerMsg::Action {
action,
terminal_id: None,
client_id,
is_cli_client: false,
});
self.exit(ExitReason::NormalDetached);
should_break = true;
},
Action::SwitchSession { .. } => {
self.os_input.send_to_server(ClientToServerMsg::Action {
action,
terminal_id: None,
client_id,
is_cli_client: false,
});
self.exit(ExitReason::NormalDetached);
should_break = true;
},
Action::CloseFocus
| Action::SwitchToMode { .. }
| Action::ClearScreen
| Action::NewPane { .. }
| Action::Run { .. }
| Action::NewTiledPane { .. }
| Action::NewFloatingPane { .. }
| Action::ToggleFloatingPanes
| Action::TogglePaneEmbedOrFloating
| Action::NewTab { .. }
| Action::GoToNextTab
| Action::GoToPreviousTab
| Action::CloseTab
| Action::GoToTab { .. }
| Action::MoveTab { .. }
| Action::GoToTabName { .. }
| Action::ToggleTab
| Action::MoveFocusOrTab { .. } => {
self.command_is_executing.blocking_input_thread();
self.os_input.send_to_server(ClientToServerMsg::Action {
action,
terminal_id: None,
client_id,
is_cli_client: false,
});
self.command_is_executing
.wait_until_input_thread_is_unblocked();
},
Action::ToggleMouseMode => {
if self.mouse_mode_active {
self.os_input.disable_mouse().non_fatal();
self.mouse_mode_active = false;
} else {
self.os_input.enable_mouse().non_fatal();
self.mouse_mode_active = true;
}
},
_ => self.os_input.send_to_server(ClientToServerMsg::Action {
action,
terminal_id: None,
client_id,
is_cli_client: false,
}),
}
should_break
}
fn exit(&mut self, reason: ExitReason) {
self.send_client_instructions
.send(ClientInstruction::Exit(reason))
.unwrap();
}
}
pub(crate) fn input_loop(
os_input: Box<dyn ClientOsApi>,
config: Config,
options: Options,
command_is_executing: CommandIsExecuting,
send_client_instructions: SenderWithContext<ClientInstruction>,
default_mode: InputMode,
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
) {
let _handler = InputHandler::new(
os_input,
command_is_executing,
config,
options,
send_client_instructions,
default_mode,
receive_input_instructions,
)
.handle_input();
}