diff options
author | Thomas Linford <tlinford@users.noreply.github.com> | 2021-07-02 16:40:50 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-02 16:40:50 +0200 |
commit | f93308f211565fae527f9b1e5788e12d0ea887c4 (patch) | |
tree | edb915be32a81c1caae2c7218398c96344a29bd0 /zellij-client/src | |
parent | 2a024db839e3be60f9428d39f6e84c7e748162d0 (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/src')
-rw-r--r-- | zellij-client/src/input_handler.rs | 49 | ||||
-rw-r--r-- | zellij-client/src/lib.rs | 3 | ||||
-rw-r--r-- | zellij-client/src/os_input_output.rs | 81 | ||||
-rw-r--r-- | zellij-client/src/unit/input_handler_tests.rs | 10 |
4 files changed, 136 insertions, 7 deletions
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, |