summaryrefslogtreecommitdiffstats
path: root/zellij-client
diff options
context:
space:
mode:
authorThomas Linford <tlinford@users.noreply.github.com>2021-07-02 16:40:50 +0200
committerGitHub <noreply@github.com>2021-07-02 16:40:50 +0200
commitf93308f211565fae527f9b1e5788e12d0ea887c4 (patch)
treeedb915be32a81c1caae2c7218398c96344a29bd0 /zellij-client
parent2a024db839e3be60f9428d39f6e84c7e748162d0 (diff)
feat(ui): initial mouse support (#448)
* Initial mouse support * enable mouse support (termion MouseTerminal) * handle scroll up and down events * Allow enabling/disabling of mouse reporting Store the mouse terminal on OsInputOutput * WIP: switch pane focus with mouse * testing to get mouse character selection * wip mouse selection * wip: mouse selection - initial handling of mouse events for text selection within a pane - wide characters currently problematic - selection is not highlighted * highlight currently selected text * improve get currently selected text from TerminalPane * copy selection to clipboard (wayland only for now) * Add missing set_should_render on selection update * Improve text selection - Strip whitespace from end of lines - Insert newlines when selection spans multiple lines * Simplify selection logic - Remove Range struct - Selection is not an Option anymore, it's empty when start == end * copy selection to clipboard with OSC-52 sequence * Improve text selection with multiple panes - Constrain mouse hold and release events to currently active pane - Fix calculation of columns with side by side panes * fix for PositionAndSize changes * Fix mouse selection with full screen pane. - Transform mouse event positions to relative when passing to pane. * Move selection to grid, rework highlighting. - Mark selected lines for update in grid output buffer, for now in the simplest way possible, but can be made more efficient by calculating changed lines only. - Clear selection on pane resize. - Re-add logic to forward mouse hold and release events only to currently active pane. * Tidy up selection - add method to get selection line range - add method to sort the current selection * Grid: move current selection up/down when scrolling - Make the selection work with lines in lines_above and lines_below - Todo: update selection end when scrolling with mouse button being held - Todo: figure out how to move selection when new characters are added. * Grid: move selection when new lines are added * Keep track of selection being active - Handle the selection growing/shrinking when scrolling with mouse button held down but not yet released. * Improve selection end with multiple panes * Tidying up - remove logging statements, unused code * Add some unit tests for selection, move position to zellij-utils * Change Position::new to take i32 for line * Grid: add unit tests for copy from selection * add basic integration test for mouse pane focus * Add basic integration test for mouse scroll * Use color from palette for selection rendering * Improve performance of selection render - Try to minimize lines to update on selection start/update/end * fixes for updated start method * fix lines not in viewport being marked for rendering - the previous optimization to grid selection rendering was always adding the lines at the extremes of previous selection, causing problems when scrolling up or down - make sure to only add lines in viewport * Disable mouse mode on exit * use saturating_sub for usize subtractions * copy selection to clipboard on mouse release * disable mouse on exit after error * remove left-over comments * remove copy keybinding * add default impl for selection methods in Pane - remove the useless methods from Impl Pane in PluginPane * move line diff between selections to selection * add option to disable mouse mode * Allow scrolling with mouse while selecting. * move action repeater to os_input_output, change timeout to 10ms - change repeater to take an action instead of a position with hardcoded action * replace mouse mode integration tests with e2e tests * cleanup * cleanup * check if mouse mode is disabled from merged options * fix missing changes in tests, cleanup
Diffstat (limited to 'zellij-client')
-rw-r--r--zellij-client/Cargo.toml1
-rw-r--r--zellij-client/src/input_handler.rs49
-rw-r--r--zellij-client/src/lib.rs3
-rw-r--r--zellij-client/src/os_input_output.rs81
-rw-r--r--zellij-client/src/unit/input_handler_tests.rs10
5 files changed, 137 insertions, 7 deletions
diff --git a/zellij-client/Cargo.toml b/zellij-client/Cargo.toml
index 9b04104b3..4ca49bccf 100644
--- a/zellij-client/Cargo.toml
+++ b/zellij-client/Cargo.toml
@@ -9,6 +9,7 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+mio = "0.7.11"
termbg = "0.2.3"
zellij-utils = { path = "../zellij-utils/", version = "0.14.0" }
diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs
index e563e3458..f02b5ad81 100644
--- a/zellij-client/src/input_handler.rs
+++ b/zellij-client/src/input_handler.rs
@@ -1,6 +1,12 @@
//! Main input logic.
-use zellij_utils::{termion, zellij_tile};
+use zellij_utils::{
+ input::{
+ mouse::{MouseButton, MouseEvent},
+ options::Options,
+ },
+ termion, zellij_tile,
+};
use crate::{os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting};
use zellij_utils::{
@@ -20,6 +26,7 @@ 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,
@@ -32,6 +39,7 @@ impl InputHandler {
os_input: Box<dyn ClientOsApi>,
command_is_executing: CommandIsExecuting,
config: Config,
+ options: Options,
send_client_instructions: SenderWithContext<ClientInstruction>,
mode: InputMode,
) -> Self {
@@ -39,6 +47,7 @@ impl InputHandler {
mode,
os_input,
config,
+ options,
command_is_executing,
send_client_instructions,
should_exit: false,
@@ -54,6 +63,10 @@ impl InputHandler {
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
+
+ if !self.options.disable_mouse_mode {
+ self.os_input.enable_mouse();
+ }
loop {
if self.should_exit {
break;
@@ -66,6 +79,10 @@ impl InputHandler {
let key = cast_termion_key(key);
self.handle_key(&key, raw_bytes);
}
+ termion::event::Event::Mouse(me) => {
+ let mouse_event = zellij_utils::input::mouse::MouseEvent::from(me);
+ self.handle_mouse_event(&mouse_event);
+ }
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
@@ -82,10 +99,6 @@ impl InputHandler {
self.handle_unknown_key(raw_bytes);
}
}
- termion::event::Event::Mouse(_) => {
- // Mouse events aren't implemented yet,
- // use a NoOp untill then.
- }
},
Err(err) => panic!("Encountered read error: {:?}", err),
}
@@ -117,6 +130,30 @@ impl InputHandler {
}
}
}
+ fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) {
+ match *mouse_event {
+ MouseEvent::Press(button, point) => match button {
+ MouseButton::WheelUp => {
+ self.dispatch_action(Action::ScrollUpAt(point));
+ }
+ MouseButton::WheelDown => {
+ self.dispatch_action(Action::ScrollDownAt(point));
+ }
+ MouseButton::Left => {
+ self.dispatch_action(Action::LeftClick(point));
+ }
+ _ => {}
+ },
+ MouseEvent::Release(point) => {
+ self.dispatch_action(Action::MouseRelease(point));
+ }
+ MouseEvent::Hold(point) => {
+ self.dispatch_action(Action::MouseHold(point));
+ self.os_input
+ .start_action_repeater(Action::MouseHold(point));
+ }
+ }
+ }
/// Dispatches an [`Action`].
///
@@ -180,6 +217,7 @@ impl InputHandler {
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,
@@ -188,6 +226,7 @@ pub(crate) fn input_loop(
os_input,
command_is_executing,
config,
+ options,
send_client_instructions,
default_mode,
)
diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs
index 5b409e750..f3e42a741 100644
--- a/zellij-client/src/lib.rs
+++ b/zellij-client/src/lib.rs
@@ -187,6 +187,7 @@ pub fn start_client(
input_loop(
os_input,
config,
+ config_options,
command_is_executing,
send_client_instructions,
default_mode,
@@ -242,6 +243,7 @@ pub fn start_client(
os_input.unset_raw_mode(0);
let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1);
let restore_snapshot = "\u{1b}[?1049l";
+ os_input.disable_mouse();
let error = format!(
"{}\n{}{}",
goto_start_of_last_line, restore_snapshot, backtrace
@@ -300,6 +302,7 @@ pub fn start_client(
goto_start_of_last_line, restore_snapshot, reset_style, show_cursor, exit_msg
);
+ os_input.disable_mouse();
os_input.unset_raw_mode(0);
let mut stdout = os_input.get_stdout_writer();
let _ = stdout.write(goodbye_message.as_bytes()).unwrap();
diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs
index 405422abc..7242bf7aa 100644
--- a/zellij-client/src/os_input_output.rs
+++ b/zellij-client/src/os_input_output.rs
@@ -1,14 +1,16 @@
-use zellij_utils::{interprocess, libc, nix, signal_hook, zellij_tile};
+use zellij_utils::input::actions::Action;
+use zellij_utils::{interprocess, libc, nix, signal_hook, termion, zellij_tile};
use interprocess::local_socket::LocalSocketStream;
+use mio::{unix::SourceFd, Events, Interest, Poll, Token};
use nix::pty::Winsize;
use nix::sys::termios;
use signal_hook::{consts::signal::*, iterator::Signals};
-use std::io;
use std::io::prelude::*;
use std::os::unix::io::RawFd;
use std::path::Path;
use std::sync::{Arc, Mutex};
+use std::{io, time};
use zellij_tile::data::Palette;
use zellij_utils::{
errors::ErrorContext,
@@ -60,6 +62,7 @@ pub struct ClientOsInputOutput {
orig_termios: Arc<Mutex<termios::Termios>>,
send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>,
receive_instructions_from_server: Arc<Mutex<Option<IpcReceiverWithContext<ServerToClientMsg>>>>,
+ mouse_term: Arc<Mutex<Option<termion::input::MouseTerminal<std::io::Stdout>>>>,
}
/// The `ClientOsApi` trait represents an abstract interface to the features of an operating system that
@@ -88,6 +91,10 @@ pub trait ClientOsApi: Send + Sync {
/// Establish a connection with the server socket.
fn connect_to_server(&self, path: &Path);
fn load_palette(&self) -> Palette;
+ fn enable_mouse(&self);
+ fn disable_mouse(&self);
+ // Repeatedly send action, until stdin is readable again
+ fn start_action_repeater(&mut self, action: Action);
}
impl ClientOsApi for ClientOsInputOutput {
@@ -180,6 +187,31 @@ impl ClientOsApi for ClientOsInputOutput {
// };
default_palette()
}
+ fn enable_mouse(&self) {
+ let mut mouse_term = self.mouse_term.lock().unwrap();
+ if mouse_term.is_none() {
+ *mouse_term = Some(termion::input::MouseTerminal::from(std::io::stdout()));
+ }
+ }
+
+ fn disable_mouse(&self) {
+ let mut mouse_term = self.mouse_term.lock().unwrap();
+ if mouse_term.is_some() {
+ *mouse_term = None;
+ }
+ }
+
+ fn start_action_repeater(&mut self, action: Action) {
+ let mut poller = StdinPoller::default();
+
+ loop {
+ let ready = poller.ready();
+ if ready {
+ break;
+ }
+ self.send_to_server(ClientToServerMsg::Action(action.clone()));
+ }
+ }
}
impl Clone for Box<dyn ClientOsApi> {
@@ -191,9 +223,54 @@ impl Clone for Box<dyn ClientOsApi> {
pub fn get_client_os_input() -> Result<ClientOsInputOutput, nix::Error> {
let current_termios = termios::tcgetattr(0)?;
let orig_termios = Arc::new(Mutex::new(current_termios));
+ let mouse_term = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput {
orig_termios,
send_instructions_to_server: Arc::new(Mutex::new(None)),
receive_instructions_from_server: Arc::new(Mutex::new(None)),
+ mouse_term,
})
}
+
+pub const DEFAULT_STDIN_POLL_TIMEOUT_MS: u64 = 10;
+
+struct StdinPoller {
+ poll: Poll,
+ events: Events,
+ timeout: time::Duration,
+}
+
+impl StdinPoller {
+ // use mio poll to check if stdin is readable without blocking
+ fn ready(&mut self) -> bool {
+ self.poll
+ .poll(&mut self.events, Some(self.timeout))
+ .expect("could not poll stdin for readiness");
+ for event in &self.events {
+ if event.token() == Token(0) && event.is_readable() {
+ return true;
+ }
+ }
+ false
+ }
+}
+
+impl Default for StdinPoller {
+ fn default() -> Self {
+ let stdin = 0;
+ let mut stdin_fd = SourceFd(&stdin);
+ let events = Events::with_capacity(128);
+ let poll = Poll::new().unwrap();
+ poll.registry()
+ .register(&mut stdin_fd, Token(0), Interest::READABLE)
+ .expect("could not create stdin poll");
+
+ let timeout = time::Duration::from_millis(DEFAULT_STDIN_POLL_TIMEOUT_MS);
+
+ Self {
+ poll,
+ events,
+ timeout,
+ }
+ }
+}
diff --git a/zellij-client/src/unit/input_handler_tests.rs b/zellij-client/src/unit/input_handler_tests.rs
index bf9bc45ab..f40344fe4 100644
--- a/zellij-client/src/unit/input_handler_tests.rs
+++ b/zellij-client/src/unit/input_handler_tests.rs
@@ -1,6 +1,7 @@
use super::input_loop;
use zellij_utils::input::actions::{Action, Direction};
use zellij_utils::input::config::Config;
+use zellij_utils::input::options::Options;
use zellij_utils::pane_size::PositionAndSize;
use zellij_utils::zellij_tile::data::Palette;
@@ -137,6 +138,9 @@ impl ClientOsApi for FakeClientOsApi {
fn load_palette(&self) -> Palette {
unimplemented!()
}
+ fn enable_mouse(&self) {}
+ fn disable_mouse(&self) {}
+ fn start_action_repeater(&mut self, _action: Action) {}
}
fn extract_actions_sent_to_server(
@@ -162,6 +166,7 @@ pub fn quit_breaks_input_loop() {
command_is_executing.clone(),
));
let config = Config::from_default_assets().unwrap();
+ let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
@@ -172,6 +177,7 @@ pub fn quit_breaks_input_loop() {
drop(input_loop(
client_os_api,
config,
+ options,
command_is_executing,
send_client_instructions,
default_mode,
@@ -196,6 +202,7 @@ pub fn move_focus_left_in_pane_mode() {
command_is_executing.clone(),
));
let config = Config::from_default_assets().unwrap();
+ let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
@@ -206,6 +213,7 @@ pub fn move_focus_left_in_pane_mode() {
drop(input_loop(
client_os_api,
config,
+ options,
command_is_executing,
send_client_instructions,
default_mode,
@@ -234,6 +242,7 @@ pub fn bracketed_paste() {
command_is_executing.clone(),
));
let config = Config::from_default_assets().unwrap();
+ let options = Options::default();
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
@@ -244,6 +253,7 @@ pub fn bracketed_paste() {
drop(input_loop(
client_os_api,
config,
+ options,
command_is_executing,
send_client_instructions,
default_mode,