From 1ca7477aa0c6f037a5b7f6527847e0c492676f87 Mon Sep 17 00:00:00 2001 From: Autumn <99296476+AutumnMeowMeow@users.noreply.github.com> Date: Tue, 14 Jan 2025 08:42:06 -0600 Subject: [PATCH] feat(terminal): mouse AnyEvent tracking (1003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch to multi-valued mouse buttons and stub for remembering old mouse button state * Stubs for passing all mouse events from user-facing terminal to server side terminal(s) in, including protobuf. Removed "held" mouse actions. Currently commented out calls to left/middle/right-click/release -- need to fix this though, as selection/copy-paste are broken too. cargo build/test/run works OK. cargo xtask build/test/run fails, unable to find crate input::mouse. * 'cargo xtask build' working using refactored functions. * fix(plugins): various cwd fixes (#3545) * fix(plugins): various cwd fixes * fix tests * docs(changelog): floating_panes cwd fix * feat(plugins): rerun_command_pane API (#3546) * feat(plugins): rerun_command_pane API * fix tests * docs(changelog): rerun command pane plugin API * feat(plugins): command pane re-run event (#3553) * docs(changelog): CommandPaneReRun plugin event * feat(ux): first run setup-wizard (#3556) * separate saved/runtime structure, kind of working * serializing config * work * work * save config through the configuration screen * work * startup wizard * style(code): cleanups * fix(session): reload config from disk when switching sessions * style(fmt): rustfmt * fix(config): propagate cli config options to screen * style(fmt): rustfmt * docs(changelog): first run setup wizard * feat(ux): reload config at runtime (#3558) * feat(ux): reload config at runtime * style(fmt): rustfmt * docs(changelog): reload config at runtime * feat(ux): change themes at runtime (#3559) * docs(changelog): change themes at runtime * feat(plugins): API to temporarily bind keys to send a message to a specific plugin id (#3561) * docs(changelog): message to specific plugins API * feat(ux): reload config options at runtime (#3564) * change simplified_ui at runtime * change default_shell at runtime * change pane_frames (from config) at runtime * all other options * some refactoring * style(fmt): rustfmt * docs(changelog): reload config options at runtime * feat(plugins): add plugin APIs to affect other panes (#3576) * resize_pane_with_id and close_pane_with_id * focus_pane_with_id and edit_scrollback_for_pane_with_id * write_to_pane_id and write_chars_to_pane_id * lots more commands * style(fmt): rustfmt * docs(changelog): new plugin apis * docs(readme): update sponsors * feat(plugins): APIs to break multiple panes into a new tab or an existing tab (#3610) * feat(plugins): break multiple panes to a new tab * fix(layouts): properly ignore run instructions when breaking panes * feat(plugins): break multiple panes to existing tab * feat(apis): allow these methods to also specify whether they want focus changed to the tab * various fixes * allow specifying name for the new tab when breaking out panes * style(fmt): rustfmt * docs(changelog): break multiple panes APIs * feat(config): allow loading background plugins on startup (#3616) * remove old partial implementation * feat(plugins): allow loading background plugins on startup * add e2e test * update config * udpate config merging * style(fmt): rustfmt * docs(changelog): background plugins * feat(ui): built-in plugin manager (#3633) * add plugin list to session info * feat(plugins): new_plugin and reload_plugin API commands * feat(plugins): built-in plugin manager * style(fmt): rustfmt * update plugins * docs(changelog): plugin-manager * fix(resurrection): various serialization issues (#3636) * fix(serialization): use kdl-rs for serialization * style(fmt): remove dead code * tests(serialization): update snapshots * style(fmt): rustfmt * docs(changelog): resurrection fixes * fix(http): web requests (#3643) * docs(changelog): http fix * feat(cli): make --layout idempotent(-ish) (#3650) * feat(cli): if inside a session, apply --layout to the session * fix(screen): some focusing races when switching tab focus * style(fmt): rustfmt * docs(changelog): improve --layout flag * fix(plugins): handle race when setting plugin selectable (#3651) * docs(changelog): plugin selectable race * feat(cli): show CACHE_DIR in `zellij setup --check` (#3652) * docs(changelog): add cache dir to setup * fix(ui): various pane name fixes (#3653) * docs(changelog): pane name fixes * fix(ux): only damage the relevant swap layout layer when resizing panes (#3654) * docs(changelog): swap layout damage fix * fix(ui): set background color for UI components according to theme (#3658) * docs(changelog): ui component fix * fix(tab): recover from crash when resizing panes (#3659) * chore(git): Add plugin’s issue templates (#3621) Co-authored-by: Zykino <3809938+Zykino@users.noreply.github.com> * fix(ux): make sure esc works as expected in unlock-first (#3660) * fix(ux): make sure esc always drops us back to base mode * fix(ux): add locked * fix(plugins): handle concurrent http downloads (#3664) * docs(changelog): concurrent http plugin downloads * fix(plugins): various plugin api and other fixes (#3665) * fix(plugins): do not allow focusing an unselectable pane * fix(folders): make sure config and cache folders exist on app start * docs(changelog): plugin fixes * fix(plugins): force use curl system lib on macOS (#3668) * docs(changelog): fix http requests for macos * fix(resurrection): plugin alias resurrection (#3673) * fix(resurrection): make sure plugin aliases are serialized properly * style(fmt): rustfmt * docs(changelog): resurrection alias fix * fix(plugins): do not allow attaching to the same session (#3674) * docs(changelog): fix plugins attaching to same session * fix(plugins): allow switching to a new session with cwd without specifying a layout (#3676) * docs(changelog): switch_session_with_cwd plugin api * fix(config): watch/update config given with --config when appropriate (#3678) * docs(changelog): reload config flag fix * feat(plugins): rebind keys api (#3680) * feat(plugins): add API to explicitly unbind/rebind specific keys in specific modes * style(fmt): rustfmt * docs(changelog): rebind keys plugin api * fix(client): repeat retry screen instruction (#3570) * docs(changelog): sixel fix * feat(ui): rebind keys UI (#3686) * rebind action working * functional ui * responsive ui * some refactoring * properly reset ui state * minor fixes * style(fmt): rustfmt * style(fmt): remove dead code * chore(deps): update to Wasmtime 21.0.2 (#3685) This fixes a race condition which causes occasional crashes and may enable a sandbox escape: * docs(changelog): wasmtime patch upgrade * feat(plugins): add API to list clients, their focused panes and running commands/plugins (#3687) * fix(list-clients): properly show client info after a tab was closed * feat(plugins): add API to list clients, their focused panes and running commands/plugins * style(fmt): rustfmt * docs(changelog): list-clients plugin api * docs(changelog): rebind keys UI * feat(ui): bring back fullscreen indication (#3688) * feat(ui): bring back fullscreen indication * fix e2e tests * docs(changelog): bring back fullscreen indication * fix(ux): new interface/configuration touch-ups (#3691) * fix(ui): copy to clipboard in new ui * fix(ux): allow navigating out of scrolled pane * style(fmt): rustfmt * docs(changelog): ui touch-ups * fix(plugins): rebind insert (#3692) * fix(tab): recover from closing a pane outside the viewport * remap insert in plugin manager * fix(plugins): remap insert key * style(fmt): rustfmt * docs(changelog): insert rebind * fix(plugins): make sure to always render on first resize (#3693) * fix(plugins): make sure to always render on first resize * style(fmt): rustfmt * fix tests * docs(changelog): plugins first-render fix * fix(terminal): recover from partial line drop (#3695) * docs(changelog): grid fix * fix(configuration): leave notification up when base mode changes (#3696) * fix(layouts): suspend commands in remote layouts (#3697) * fix(layouts): suspend commands in remote layouts * style(fmt): rustfmt * docs(changelog): suspend commands in remote layouts * feat(plugins): add configurable black background for ui components (#3681) * feat(plugins): add transparent background for text and nested_list * chore: fix formatting issue * feat: invert flag behaviour * feat: implement bg_black handling for table cells * fix: order of selected and bg_black in protocol * chore: rename from bg_black to opaque * fix: explicit selected, if opaque and selected for text * chore: fix formatting issues * feat: opaque tab-bar * feat: opaque session-manager bars * feat: opaque ribbon in plugin manager * feat: opaque one-line ui * feat: opaque tab-bar in configuration plugin * style(fmt): various cleanups (#3698) * fix(configuration): rounding error in ui * style(fmt): remove warnings * style(fmt): rustfmt * docs(changelog): ui components bg fix * feat(theme): add theme ao (#3478) * feat(themes): add atelier sulphurpool theme (#3596) * feat(themes): added ayu mirage, light, and dark themes (#3567) Co-authored-by: Evan Lauer * feat(themes): add Vesper theme (#3443) * feat(themes): add night-owl theme (#3393) Co-authored-by: Bruno Mesquita * feat: add iceberg dark/light themes (#3323) * theme: add onedark theme to the available themes (#3313) Onedark is a popular theme from Atom text editor. I've used these sources as the reference for implementing the theme in zellij. https://github.com/joshdick/onedark.vim https://www.figma.com/community/file/1137445418485757476/atom-one-dark-color-palette * feat(themes): add basic ANSI theme (#3308) * fix(theme): fix for gruvbox light and dark (#3255) the previous themes do not have the correct colors and aren't great for the eyes. when selecting e.g. to copy text, it uses red for light theme and the same bg for dark theme. the previous light theme is not even a light theme so I also fixed that as well. Signed-off-by: Soc Virnyl Estela * feat(themes): create lucario.kdl (#3030) * docs(changelog): new themes * style(fmt): remove warnings (#3701) * fix: (tabs) move to next tab if moving to next pane from fullscreen pane (#3498) Co-authored-by: Vasilis Manolopoulos * docs(changelog): MoveFocusOrTab fullscreen awareness * docs(readme): add https to curl download * fix(ux): configuration fixes (#3713) * fix(startup): try create config folder if it doesn't exist * fix(configuration): tab bar ui * fix(configuration): rebind ctrl-s to ctrl-a * fix(configuration): remove extra rebinding leaders screen * docs(changelog): configuration fixes * fix(ux): forward keys to pane in locked mode and base mode rather than hard-coded normal mode (#3715) * fix(keybindings): only forward clear keys in locked mode and default mode * style(fmt): rustfmt * docs(changelog): base mode keybindings fix * chore(release): v0.41.0 * HOTFIX: default plugins generic compilation issue * HOTFIX: patch version * chore(release): v0.41.1 * chore(repo): bump development version * fix(input): remove support for extra modifiers (#3725) * docs(changelog): kitty input fix * fix(input): refix ctrl-j (#3746) * fix(input): refix ctrl-j * fix e2e tests * docs(changelog): refix ctrl-j * fix(plugins): cwd and usability fixes (#3749) * fix(plugins): maintain cwd between plugin reloads * fix(plugin-manager): default to loading plugins in the foreground and allow sending space in configuration * docs(changelog): plugin fixes * fix(output-buffer): truncate grid height when not rendering it fully (#3750) * fix(output-buffer): truncate grid height when not rendering it fully * also fix for cases where the changed lines are not contiguous * docs(changelog): output-buffer fix * fix(tabs): maintain event order for MoveTab (#3758) * fix(tabs): maintain event order for MoveTab * style(fmt): rustfmt * docs(changelog): event ordering fix * fix(plugins): do not open extra instances of aliases (#3759) * docs(changelog): do not duplicate built-in plugins * fix(terminal): reset kitty keyboard support when resetting terminal state (#3760) * docs(changelog): kitty reset fix * fix(config): crash if unable to watch config folder (#3761) * docs(changelog): config dir crash fix * fix(statup): slow startup on some occasions (#3767) * add debug logs * add log messages * some more logs and possible fix? * remove logs * style(fmt): rustfmt * remove comment * docs(changelog): occasional slow startup fix * fix(panes): handle various invalid state situations (#3776) * docs(changelog): invalid state handling * chore: add vendored_curl feature (#3766) * docs(changelog): vendored curl option * fix(ux): change plugin manager shortcut (#3779) * docs(changelog): shortcut change * fix(screen): send PaneClosed event to plugins also when closing the whole tab (#3781) * docs(changelog): pane-closed event * feat(plugins): add /cache folder (#3787) * feat(plugins): add /cache folder * style(fmt): rustfmt * docs(changelog): plugin cache folder * docs(changelog): plugin cache folder url * chore(package): vendor common_path (#3780) * vendoring common_path * add original license to common_path * Clarify license scope * refactor: remove rand dependency --------- Co-authored-by: Aram Drevekenin * docs(changelog): vendor common_path * fix(plugins): derive hash and ord for PaneId (#3790) * docs(changelog): derive hash and ord for paneid * style(fmt): remove warnings * chore(version): set patch version * chore(release): v0.41.2 * chore(repo): bump development version * chore(repo): fix typo in lock file * fix(plugins): properly focus pane after tab was closed (#3797) * fix(plugins): properly focus pane after tab was closed * style(fmt): rustfmt * docs(changelog): focus_pane_with_id fix * fix(plugins): properly pad UI elements when they have a background (#3806) * fix(plugins): mark selected background up until component width * style(fmt): rustfmt * docs(changelog): ui component padding * feat(plugins): allow changing the plugin's `/host` folder (under a new permission) (#3827) * working without notifying plugins * permissions and events * cleanups and formatting * style(fmt): rustfmt * docs(changelog): allow plugins to change host folder * chore(repo): add funding.json (#3838) * chore(repo): add funding.json * update funding json url * fix(plugins): do not detach if using a slash in a session name (#3839) * docs(changelog): slash detach fix * fix(plugins): properly focus plugin after it was hidden (#3841) * docs(changelog): plugin hidden focus fix * fix(screen): off by 1 error when focusing layout tab (#3844) * docs(changelog): layout tab focus fix * fix(multiuser): properly clear fake cursors (#3845) * docs(changelog): multiplayer cursor fix * feat(ux): pin floating panes (#3876) * working * ui indication * add keybinding * add to plugin panes * fix with multiple cursors * toggle with the mouse * fix e2e tests and add new one * some cleanups * add to layouts * make mouse click more lenient * allow setting a new floating pane as pinned * make toggle work throughthe command line * add to plugin api * get tests to pass * style(fmt): rustfmt * docs(changelog): pin floating panes * fix(layout-applier): logical index pane sorting (#3893) * initial draft * working with floating panes as well * use the same method for applying an initial layout to tiled panes * some refactoring * all code paths working with logical positioning fallback! * get tests to compile * get e2e tests to pass * fix e2e remote runner * breadth-first layout sorting * fix some bugs * style(fmt): rustfmt * style(fmt): remove comments * docs(changelog): logical index pane sorting * fix(terminal): mode 2026 feature detection response (#3884) The response to the 2026 mode query was missing a `?` character. The response should be of the format `CSI ? 2026 ; N $ y` where N can be any value in the range 0-4 inclusive. References: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036 https://vt100.net/docs/vt510-rm/DECRPM.html * docs(changelog): synchronized rendering query response * fix(terminal): cursor overflow (#3894) * docs(changelog): cursor overflow fix * fix(ux): make the Zellij mouse interaction work * fix(rendering): only render if selection/floating-pane position was changed * do not clear copied to clipboard message on mouse motion * various functionality fixes * fix tests * fixes and cleanups * style(fmt): rustfmt * fix(mouse): only report state to plugins when it changed * fix(plugins): send mouse clicks to inactive panes * tests: any event tracking in panes * style(fmt): rustfmt * style: remove unused stuff --------- Signed-off-by: Soc Virnyl Estela Co-authored-by: Autumn Lamonte Co-authored-by: Aram Drevekenin Co-authored-by: Zykino Co-authored-by: Zykino <3809938+Zykino@users.noreply.github.com> Co-authored-by: Daniel Jankowski Co-authored-by: Manuel de Prada Corral <6536835+manueldeprada@users.noreply.github.com> Co-authored-by: bjorn3 <17426603+bjorn3@users.noreply.github.com> Co-authored-by: Michael Jones Co-authored-by: carl <44021312+achristmascarl@users.noreply.github.com> Co-authored-by: Evthestrike <62308745+Evthestrike@users.noreply.github.com> Co-authored-by: Evan Lauer Co-authored-by: Rafael Bardini Co-authored-by: Bruno Mesquita Co-authored-by: Bruno Mesquita Co-authored-by: Chromo-residuum-opec Co-authored-by: Shone Binu <62597277+shonebinu@users.noreply.github.com> Co-authored-by: Mike Greiling Co-authored-by: Soc Virnyl S. Estela Co-authored-by: Eric Raio <43896+ericraio@users.noreply.github.com> Co-authored-by: Vasileios Manolopoulos <44965914+VasilisManol@users.noreply.github.com> Co-authored-by: Vasilis Manolopoulos Co-authored-by: tranzystorekk Co-authored-by: s1syph0s <34000276+s1syph0s@users.noreply.github.com> Co-authored-by: Darren Burns --- zellij-client/src/input_handler.rs | 174 ++--- zellij-client/src/os_input_output.rs | 6 +- zellij-server/src/output/mod.rs | 6 +- zellij-server/src/panes/floating_panes/mod.rs | 8 +- zellij-server/src/panes/grid.rs | 140 +++- zellij-server/src/panes/terminal_pane.rs | 25 +- zellij-server/src/route.rs | 66 +- zellij-server/src/screen.rs | 78 +- zellij-server/src/tab/mod.rs | 693 ++++++++++-------- .../src/tab/unit/tab_integration_tests.rs | 494 ++++++++++--- zellij-utils/assets/prost/api.action.rs | 48 +- zellij-utils/src/errors.rs | 4 +- zellij-utils/src/input/actions.rs | 19 +- zellij-utils/src/input/mod.rs | 5 +- zellij-utils/src/input/mouse.rs | 290 +++++--- zellij-utils/src/plugin_api/action.proto | 22 +- zellij-utils/src/plugin_api/action.rs | 161 ++-- 17 files changed, 1379 insertions(+), 860 deletions(-) diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index a37590639a..9df9ec5f14 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -11,26 +11,14 @@ use zellij_utils::{ actions::Action, cast_termwiz_key, config::Config, - mouse::{MouseButton, MouseEvent}, + mouse::{MouseEvent, MouseEventType}, options::Options, }, ipc::{ClientToServerMsg, ExitReason}, - termwiz::input::InputEvent, + position::Position, + termwiz::input::{InputEvent, Modifiers, MouseButtons, MouseEvent as TermwizMouseEvent}, }; -#[derive(Debug, Clone, Copy)] -enum HeldMouseButton { - Left, - Right, - Middle, -} - -impl Default for HeldMouseButton { - fn default() -> Self { - HeldMouseButton::Left - } -} - /// Handles the dispatching of [`Action`]s according to the current /// [`InputMode`], and keep tracks of the current [`InputMode`]. struct InputHandler { @@ -43,10 +31,92 @@ struct InputHandler { send_client_instructions: SenderWithContext, should_exit: bool, receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, - holding_mouse: Option, + 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); +} + +fn from_termwiz(old_event: &mut MouseEvent, event: TermwizMouseEvent) -> MouseEvent { + // We use the state of old_event vs new_event to determine if this + // event is a Press, Release, or Motion. This is an unfortunate + // side effect of the pre-SGR-encoded X10 mouse protocol design in + // which release events don't carry information about WHICH + // button(s) were released, so we have to maintain a wee bit of + // state in between events. + // + // Note that only Left, Right, and Middle are saved in between + // calls. WheelUp/WheelDown typically do not generate Release + // events. + 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 + { + // This is a mouse Press event. + new_event.event_type = MouseEventType::Press; + + // Hang onto the button state. + *old_event = new_event; + } else if event.mouse_buttons.is_empty() + && !old_event.left + && !old_event.right + && !old_event.middle + { + // This is a mouse Motion event (no buttons are down). + new_event.event_type = MouseEventType::Motion; + + // Hang onto the button state. + *old_event = new_event; + } else if event.mouse_buttons.is_empty() + && (old_event.left || old_event.right || old_event.middle) + { + // This is a mouse Release event. Note that we set + // old_event.{button} to false (to release), but set ONLY the + // new_event that were released to true before sending the + // event up. + 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 { + // Dragging with some button down. Return it as a Motion + // event, and hang on to the button state. + new_event.event_type = MouseEventType::Motion; + *old_event = new_event; + } + + new_event +} + impl InputHandler { /// Returns a new [`InputHandler`] with the attributes specified as arguments. fn new( @@ -67,7 +137,7 @@ impl InputHandler { send_client_instructions, should_exit: false, receive_input_instructions, - holding_mouse: None, + mouse_old_event: MouseEvent::new(), mouse_mode_active: false, } } @@ -99,8 +169,7 @@ impl InputHandler { self.handle_key(&key, raw_bytes, false); }, InputEvent::Mouse(mouse_event) => { - let mouse_event = - zellij_utils::input::mouse::MouseEvent::from(mouse_event); + let mouse_event = from_termwiz(&mut self.mouse_old_event, mouse_event); self.handle_mouse_event(&mouse_event); }, InputEvent::Paste(pasted_text) => { @@ -215,70 +284,9 @@ 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), None); - }, - MouseButton::WheelDown => { - self.dispatch_action(Action::ScrollDownAt(point), None); - }, - MouseButton::Left => { - if self.holding_mouse.is_some() { - self.dispatch_action(Action::MouseHoldLeft(point), None); - } else { - self.dispatch_action(Action::LeftClick(point), None); - } - self.holding_mouse = Some(HeldMouseButton::Left); - }, - MouseButton::Right => { - if self.holding_mouse.is_some() { - self.dispatch_action(Action::MouseHoldRight(point), None); - } else { - self.dispatch_action(Action::RightClick(point), None); - } - self.holding_mouse = Some(HeldMouseButton::Right); - }, - MouseButton::Middle => { - if self.holding_mouse.is_some() { - self.dispatch_action(Action::MouseHoldMiddle(point), None); - } else { - self.dispatch_action(Action::MiddleClick(point), None); - } - self.holding_mouse = Some(HeldMouseButton::Middle); - }, - }, - MouseEvent::Release(point) => { - let button_released = self.holding_mouse.unwrap_or_default(); - match button_released { - HeldMouseButton::Left => { - self.dispatch_action(Action::LeftMouseRelease(point), None) - }, - HeldMouseButton::Right => { - self.dispatch_action(Action::RightMouseRelease(point), None) - }, - HeldMouseButton::Middle => { - self.dispatch_action(Action::MiddleMouseRelease(point), None) - }, - }; - self.holding_mouse = None; - }, - MouseEvent::Hold(point) => { - let button_held = self.holding_mouse.unwrap_or_default(); - match button_held { - HeldMouseButton::Left => { - self.dispatch_action(Action::MouseHoldLeft(point), None) - }, - HeldMouseButton::Right => { - self.dispatch_action(Action::MouseHoldRight(point), None) - }, - HeldMouseButton::Middle => { - self.dispatch_action(Action::MouseHoldMiddle(point), None) - }, - }; - self.holding_mouse = Some(button_held); - }, - } + // This dispatch handles all of the output(s) to terminal + // pane(s). + self.dispatch_action(Action::MouseEvent(*mouse_event), None); } /// Dispatches an [`Action`]. /// diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs index 47dcafb091..41d03b0f72 100644 --- a/zellij-client/src/os_input_output.rs +++ b/zellij-client/src/os_input_output.rs @@ -22,8 +22,10 @@ use zellij_utils::{ const SIGWINCH_CB_THROTTLE_DURATION: time::Duration = time::Duration::from_millis(50); -const ENABLE_MOUSE_SUPPORT: &str = "\u{1b}[?1000h\u{1b}[?1002h\u{1b}[?1015h\u{1b}[?1006h"; -const DISABLE_MOUSE_SUPPORT: &str = "\u{1b}[?1006l\u{1b}[?1015l\u{1b}[?1002l\u{1b}[?1000l"; +const ENABLE_MOUSE_SUPPORT: &str = + "\u{1b}[?1000h\u{1b}[?1002h\u{1b}[?1003h\u{1b}[?1015h\u{1b}[?1006h"; +const DISABLE_MOUSE_SUPPORT: &str = + "\u{1b}[?1006l\u{1b}[?1015l\u{1b}[?1003l\u{1b}[?1002l\u{1b}[?1000l"; fn into_raw_mode(pid: RawFd) { let mut tio = termios::tcgetattr(pid).expect("could not get terminal attribute"); diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs index 32192a203a..38a1585bd9 100644 --- a/zellij-server/src/output/mod.rs +++ b/zellij-server/src/output/mod.rs @@ -578,8 +578,12 @@ impl FloatingPanesStack { .with_context(err_context)?; let left_chunk_x = c_chunk_left_side; let right_chunk_x = pane_right_edge + 1; - let left_chunk = + let mut left_chunk = CharacterChunk::new(left_chunk_characters, left_chunk_x, c_chunk.y); + if !c_chunk.selection_and_colors.is_empty() { + left_chunk.selection_and_colors = c_chunk.selection_and_colors.clone(); + } + c_chunk.x = right_chunk_x; c_chunk.terminal_characters = right_chunk_characters; return Ok(Some(left_chunk)); diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 27eda0964d..ff4ca26c4d 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -898,9 +898,11 @@ impl FloatingPanes { // true => handled, false => not handled (eg. no pane at this position) let show_panes = self.show_panes; if self.pane_being_moved_with_mouse.is_some() { - self.move_pane_to_position(&position); - self.set_force_render(); - return true; + if self.move_pane_to_position(&position) { + // pane was moved to a new position + self.set_force_render(); + return true; + } } else if let Some(pane) = self.get_pane_at_mut(&position, search_selectable) { let clicked_on_frame = pane.position_is_on_frame(&position); if show_panes && clicked_on_frame { diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 62d2e53373..95c3be59ba 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -17,6 +17,7 @@ use std::{ use zellij_utils::{ consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, data::{Palette, PaletteColor}, + input::mouse::{MouseEvent, MouseEventType}, pane_size::SizeInPixels, position::Position, vte, @@ -385,6 +386,7 @@ pub enum MouseTracking { Off, Normal, ButtonEventTracking, + AnyEventTracking, } impl Default for MouseTracking { @@ -1743,9 +1745,11 @@ impl Grid { } pub fn update_selection(&mut self, to: &Position) { let old_selection = self.selection; - self.selection.to(*to); - self.update_selected_lines(&old_selection, &self.selection.clone()); - self.mark_for_rerender(); + if &old_selection.end != to { + self.selection.to(*to); + self.update_selected_lines(&old_selection, &self.selection.clone()); + self.mark_for_rerender(); + } } pub fn end_selection(&mut self, end: &Position) { @@ -1930,6 +1934,96 @@ impl Grid { } } } + fn mouse_buttons_value_x10(&self, event: &MouseEvent) -> u8 { + let mut value = 35; // Default to no buttons down. + if event.event_type == MouseEventType::Release { + return value; + } + if event.left { + value = 32; + } else if event.middle { + value = 33; + } else if event.right { + value = 34; + } else if event.wheel_up { + value = 68; + } else if event.wheel_down { + value = 69; + } + if event.event_type == MouseEventType::Motion { + value += 32; + } + if event.shift { + value |= 0x04; + } + if event.alt { + value |= 0x08; + } + if event.ctrl { + value |= 0x10; + } + value + } + fn mouse_buttons_value_sgr(&self, event: &MouseEvent) -> u8 { + let mut value = 3; // Default to no buttons down. + if event.left { + value = 0; + } else if event.middle { + value = 1; + } else if event.right { + value = 2; + } else if event.wheel_up { + value = 64; + } else if event.wheel_down { + value = 65; + } + if event.event_type == MouseEventType::Motion { + value += 32; + } + if event.shift { + value |= 0x04; + } + if event.alt { + value |= 0x08; + } + if event.ctrl { + value |= 0x10; + } + value + } + pub fn mouse_event_signal(&self, event: &MouseEvent) -> Option { + let emit = match (&self.mouse_tracking, event.event_type) { + (MouseTracking::Off, _) => false, + (MouseTracking::AnyEventTracking, _) => true, + (_, MouseEventType::Press | MouseEventType::Release) => true, + (MouseTracking::ButtonEventTracking, MouseEventType::Motion) => { + event.left | event.right | event.middle | event.wheel_up | event.wheel_down + }, + (_, _) => false, + }; + + match (emit, &self.mouse_mode) { + (true, MouseMode::NoEncoding | MouseMode::Utf8) => { + let mut msg: Vec = vec![27, b'[', b'M', self.mouse_buttons_value_x10(event)]; + msg.append(&mut utf8_mouse_coordinates( + event.position.column() + 1, + event.position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }, + (true, MouseMode::Sgr) => Some(format!( + "\u{1b}[<{:?};{:?};{:?}{}", + self.mouse_buttons_value_sgr(event), + event.position.column() + 1, + event.position.line() + 1, + match event.event_type { + MouseEventType::Press | MouseEventType::Motion => 'M', + _ => 'm', + } + )), + (_, _) => None, + } + } pub fn mouse_left_click_signal(&self, position: &Position, is_held: bool) -> Option { let utf8_event = || -> Option { let button_code = if is_held { b'@' } else { b' ' }; @@ -1954,10 +2048,14 @@ impl Grid { (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => { utf8_event() }, - (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => { - utf8_event() - }, - (MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(), + ( + MouseMode::NoEncoding | MouseMode::Utf8, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => utf8_event(), + ( + MouseMode::Sgr, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => sgr_event(), (MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), _ => None, } @@ -2007,10 +2105,14 @@ impl Grid { (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => { utf8_event() }, - (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => { - utf8_event() - }, - (MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(), + ( + MouseMode::NoEncoding | MouseMode::Utf8, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => utf8_event(), + ( + MouseMode::Sgr, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => sgr_event(), (MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), _ => None, } @@ -2060,10 +2162,14 @@ impl Grid { (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::Normal) if !is_held => { utf8_event() }, - (MouseMode::NoEncoding | MouseMode::Utf8, MouseTracking::ButtonEventTracking) => { - utf8_event() - }, - (MouseMode::Sgr, MouseTracking::ButtonEventTracking) => sgr_event(), + ( + MouseMode::NoEncoding | MouseMode::Utf8, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => utf8_event(), + ( + MouseMode::Sgr, + MouseTracking::ButtonEventTracking | MouseTracking::AnyEventTracking, + ) => sgr_event(), (MouseMode::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), _ => None, } @@ -2605,7 +2711,7 @@ impl Perform for Grid { self.mouse_tracking = MouseTracking::Off; }, 1003 => { - // TBD: any-even mouse tracking + self.mouse_tracking = MouseTracking::Off; }, 1004 => { self.focus_event_tracking = false; @@ -2708,7 +2814,7 @@ impl Perform for Grid { self.mouse_tracking = MouseTracking::ButtonEventTracking; }, 1003 => { - // TBD: any-even mouse tracking + self.mouse_tracking = MouseTracking::AnyEventTracking; }, 1004 => { self.focus_event_tracking = true; diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 2e825ac84c..61587d5eb8 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -14,6 +14,7 @@ use std::fmt::Debug; use std::rc::Rc; use std::time::{self, Instant}; use zellij_utils::input::command::RunCommand; +use zellij_utils::input::mouse::{MouseEvent, MouseEventType}; use zellij_utils::pane_size::Offset; use zellij_utils::{ data::{ @@ -575,14 +576,16 @@ impl Pane for TerminalPane { if cursor_at_the_bottom { self.grid.scroll_up_one_line(); self.selection_scrolled_at = time::Instant::now(); + self.set_should_render(true); } else if cursor_at_the_top { self.grid.scroll_down_one_line(); self.selection_scrolled_at = time::Instant::now(); + self.set_should_render(true); } else if cursor_in_the_middle { + // here we'll only render if the selection was updated, and that'll be handled by the + // grid self.grid.update_selection(to); } - - self.set_should_render(true); } fn end_selection(&mut self, end: &Position, _client_id: ClientId) { @@ -633,6 +636,10 @@ impl Pane for TerminalPane { self.exclude_from_sync } + fn mouse_event(&self, event: &MouseEvent) -> Option { + self.grid.mouse_event_signal(event) + } + fn mouse_left_click(&self, position: &Position, is_held: bool) -> Option { self.grid.mouse_left_click_signal(position, is_held) } @@ -842,6 +849,20 @@ impl Pane for TerminalPane { } false } + fn intercept_mouse_event_on_frame(&mut self, event: &MouseEvent, client_id: ClientId) -> bool { + if self.position_is_on_frame(&event.position) { + let relative_position = self.relative_position(&event.position); + if let MouseEventType::Press = event.event_type { + if let Some(client_frame) = self.frame.get_mut(&client_id) { + if client_frame.clicked_on_pinned(relative_position) { + self.toggle_pinned(); + return true; + } + } + } + } + false + } fn reset_logical_position(&mut self) { self.geom.logical_position = None; } diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index d611fdcdb8..7c65491b78 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -47,20 +47,14 @@ pub(crate) fn route_action( let mut should_break = false; let err_context = || format!("failed to route action for client {client_id}"); - // forward the action to plugins unless it is a mousehold - // this is a bit of a hack around the unfortunate architecture we use with plugins - // this will change as soon as we refactor - match action { - Action::MouseHoldLeft(..) | Action::MouseHoldRight(..) => {}, - _ => { - senders - .send_to_plugin(PluginInstruction::Update(vec![( - None, - Some(client_id), - Event::InputReceived, - )])) - .with_context(err_context)?; - }, + if !action.is_mouse_motion() { + senders + .send_to_plugin(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::InputReceived, + )])) + .with_context(err_context)?; } match action { @@ -592,49 +586,9 @@ pub(crate) fn route_action( .with_context(err_context)?; should_break = true; }, - Action::LeftClick(point) => { - senders - .send_to_screen(ScreenInstruction::LeftClick(point, client_id)) - .with_context(err_context)?; - }, - Action::RightClick(point) => { - senders - .send_to_screen(ScreenInstruction::RightClick(point, client_id)) - .with_context(err_context)?; - }, - Action::MiddleClick(point) => { - senders - .send_to_screen(ScreenInstruction::MiddleClick(point, client_id)) - .with_context(err_context)?; - }, - Action::LeftMouseRelease(point) => { - senders - .send_to_screen(ScreenInstruction::LeftMouseRelease(point, client_id)) - .with_context(err_context)?; - }, - Action::RightMouseRelease(point) => { - senders - .send_to_screen(ScreenInstruction::RightMouseRelease(point, client_id)) - .with_context(err_context)?; - }, - Action::MiddleMouseRelease(point) => { - senders - .send_to_screen(ScreenInstruction::MiddleMouseRelease(point, client_id)) - .with_context(err_context)?; - }, - Action::MouseHoldLeft(point) => { - senders - .send_to_screen(ScreenInstruction::MouseHoldLeft(point, client_id)) - .with_context(err_context)?; - }, - Action::MouseHoldRight(point) => { - senders - .send_to_screen(ScreenInstruction::MouseHoldRight(point, client_id)) - .with_context(err_context)?; - }, - Action::MouseHoldMiddle(point) => { + Action::MouseEvent(event) => { senders - .send_to_screen(ScreenInstruction::MouseHoldMiddle(point, client_id)) + .send_to_screen(ScreenInstruction::MouseEvent(event, client_id)) .with_context(err_context)?; }, Action::Copy => { diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 7f1e044a2d..27de5fa0d3 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -15,6 +15,7 @@ use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; use zellij_utils::input::config::Config; use zellij_utils::input::keybinds::Keybinds; +use zellij_utils::input::mouse::MouseEvent; use zellij_utils::input::options::Clipboard; use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::{ @@ -249,15 +250,7 @@ pub enum ScreenInstruction { TerminalColorRegisters(Vec<(usize, String)>), ChangeMode(ModeInfo, ClientId), ChangeModeForAllClients(ModeInfo), - LeftClick(Position, ClientId), - RightClick(Position, ClientId), - MiddleClick(Position, ClientId), - LeftMouseRelease(Position, ClientId), - RightMouseRelease(Position, ClientId), - MiddleMouseRelease(Position, ClientId), - MouseHoldLeft(Position, ClientId), - MouseHoldRight(Position, ClientId), - MouseHoldMiddle(Position, ClientId), + MouseEvent(MouseEvent, ClientId), Copy(ClientId), AddClient( ClientId, @@ -522,15 +515,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ToggleActiveSyncTab(..) => ScreenContext::ToggleActiveSyncTab, ScreenInstruction::ScrollUpAt(..) => ScreenContext::ScrollUpAt, ScreenInstruction::ScrollDownAt(..) => ScreenContext::ScrollDownAt, - ScreenInstruction::LeftClick(..) => ScreenContext::LeftClick, - ScreenInstruction::RightClick(..) => ScreenContext::RightClick, - ScreenInstruction::MiddleClick(..) => ScreenContext::MiddleClick, - ScreenInstruction::LeftMouseRelease(..) => ScreenContext::LeftMouseRelease, - ScreenInstruction::RightMouseRelease(..) => ScreenContext::RightMouseRelease, - ScreenInstruction::MiddleMouseRelease(..) => ScreenContext::MiddleMouseRelease, - ScreenInstruction::MouseHoldLeft(..) => ScreenContext::MouseHoldLeft, - ScreenInstruction::MouseHoldRight(..) => ScreenContext::MouseHoldRight, - ScreenInstruction::MouseHoldMiddle(..) => ScreenContext::MouseHoldMiddle, + ScreenInstruction::MouseEvent(..) => ScreenContext::MouseEvent, ScreenInstruction::Copy(..) => ScreenContext::Copy, ScreenInstruction::ToggleTab(..) => ScreenContext::ToggleTab, ScreenInstruction::AddClient(..) => ScreenContext::AddClient, @@ -3753,56 +3738,13 @@ pub(crate) fn screen_thread_main( screen.render(None)?; screen.unblock_input()?; }, - ScreenInstruction::LeftClick(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_left_click(&point, client_id), ?); - screen.log_and_report_session_state()?; - screen.render(None)?; - screen.unblock_input()?; - }, - ScreenInstruction::RightClick(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_right_click(&point, client_id), ?); - screen.log_and_report_session_state()?; - screen.render(None)?; - screen.unblock_input()?; - }, - ScreenInstruction::MiddleClick(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_middle_click(&point, client_id), ?); - screen.log_and_report_session_state()?; - screen.render(None)?; - screen.unblock_input()?; - }, - ScreenInstruction::LeftMouseRelease(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_left_mouse_release(&point, client_id), ?); - screen.render(None)?; - screen.unblock_input()?; - }, - ScreenInstruction::RightMouseRelease(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_right_mouse_release(&point, client_id), ?); - screen.render(None)?; - }, - ScreenInstruction::MiddleMouseRelease(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_middle_mouse_release(&point, client_id), ?); - screen.render(None)?; - }, - ScreenInstruction::MouseHoldLeft(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_mouse_hold_left(&point, client_id), ?); - screen.render(None)?; - }, - ScreenInstruction::MouseHoldRight(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_mouse_hold_right(&point, client_id), ?); - screen.render(None)?; - }, - ScreenInstruction::MouseHoldMiddle(point, client_id) => { - active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_mouse_hold_middle(&point, client_id), ?); + ScreenInstruction::MouseEvent(event, client_id) => { + let state_changed = screen + .get_active_tab_mut(client_id) + .and_then(|tab| tab.handle_mouse_event(&event, client_id))?; + if state_changed { + screen.log_and_report_session_state()?; + } screen.render(None)?; }, ScreenInstruction::Copy(client_id) => { diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index bb3e8a7f1c..2c0fed76b6 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -16,6 +16,7 @@ use zellij_utils::data::{ }; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; +use zellij_utils::input::mouse::{MouseEvent, MouseEventType}; use zellij_utils::position::{Column, Line}; use zellij_utils::{position::Position, serde}; @@ -168,7 +169,7 @@ pub(crate) struct Tab { draw_pane_frames: bool, auto_layout: bool, pending_vte_events: HashMap>, - pub selecting_with_mouse: bool, // this is only pub for the tests TODO: remove this once we combine write_text_to_clipboard with render + pub selecting_with_mouse_in_pane: Option, // this is only pub for the tests link_handler: Rc>, clipboard_provider: ClipboardProvider, // TODO: used only to focus the pane when the layout is loaded @@ -397,10 +398,19 @@ pub trait Pane { } false } + // TODO: get rid of this in favor of intercept_mouse_event_on_frame fn intercept_left_mouse_click(&mut self, _position: &Position, _client_id: ClientId) -> bool { let intercepted = false; intercepted } + fn intercept_mouse_event_on_frame( + &mut self, + _event: &MouseEvent, + _client_id: ClientId, + ) -> bool { + let intercepted = false; + intercepted + } fn store_pane_name(&mut self); fn load_pane_name(&mut self); fn set_borderless(&mut self, borderless: bool); @@ -410,6 +420,9 @@ pub trait Pane { // TODO: this should probably be merged with the mouse_right_click fn handle_right_click(&mut self, _to: &Position, _client_id: ClientId) {} + fn mouse_event(&self, _event: &MouseEvent) -> Option { + None + } fn mouse_left_click(&self, _position: &Position, _is_held: bool) -> Option { None } @@ -646,7 +659,7 @@ impl Tab { auto_layout, pending_vte_events: HashMap::new(), connected_clients, - selecting_with_mouse: false, + selecting_with_mouse_in_pane: None, link_handler: Rc::new(RefCell::new(LinkHandler::new())), clipboard_provider, focus_pane_id: None, @@ -3171,7 +3184,7 @@ impl Tab { point: &Position, lines: usize, client_id: ClientId, - ) -> Result<()> { + ) -> Result { let err_context = || { format!("failed to handle scrollwheel up at position {point:?} for client {client_id}") }; @@ -3192,7 +3205,7 @@ impl Tab { pane.scroll_up(lines, client_id); } } - Ok(()) + Ok(false) } pub fn handle_scrollwheel_down( @@ -3200,7 +3213,7 @@ impl Tab { point: &Position, lines: usize, client_id: ClientId, - ) -> Result<()> { + ) -> Result { let err_context = || { format!( "failed to handle scrollwheel down at position {point:?} for client {client_id}" @@ -3229,7 +3242,7 @@ impl Tab { } } } - Ok(()) + Ok(false) } fn get_pane_at( @@ -3297,139 +3310,404 @@ impl Tab { } } - pub fn handle_left_click(&mut self, position: &Position, client_id: ClientId) -> Result<()> { - let err_context = || { - format!( - "failed to handle mouse left click at position {position:?} for client {client_id}" - ) - }; + // returns true if the mouse event caused some sort of tab/pane state change that needs to be + // reported to plugins + pub fn handle_mouse_event(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or(anyhow!("Failed to find pane at position"))?; - let intercepted = self - .get_pane_at(position, false) + if event.left { + // left mouse click + let pane_id_at_position = self + .get_pane_at(&event.position, false) + .with_context(err_context)? + .ok_or_else(|| anyhow!("Failed to find pane at position"))? + .pid(); + match event.event_type { + MouseEventType::Press => { + if pane_id_at_position == active_pane_id { + self.handle_active_pane_left_mouse_press(event, client_id) + } else { + self.handle_inactive_pane_left_mouse_press(event, client_id) + } + }, + MouseEventType::Motion => self.handle_left_mouse_motion(event, client_id), + MouseEventType::Release => self.handle_left_mouse_release(event, client_id), + } + } else if event.wheel_up { + self.handle_scrollwheel_up(&event.position, 3, client_id) + } else if event.wheel_down { + self.handle_scrollwheel_down(&event.position, 3, client_id) + } else if event.right { + self.handle_right_click(&event, client_id) + } else if event.middle { + self.handle_middle_click(&event, client_id) + } else { + self.handle_mouse_no_click(&event, client_id) + } + } + fn write_mouse_event_to_active_pane( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result<()> { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); + if let Some(active_pane) = active_pane { + let relative_position = active_pane.relative_position(&event.position); + let mut pass_event = *event; + pass_event.position = relative_position; + if let Some(mouse_event) = active_pane.mouse_event(&pass_event) { + if !active_pane.position_is_on_frame(&event.position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } + } + } + Ok(()) + } + // returns true if the mouse event caused some sort of tab/pane state change that needs to be + // reported to plugins + fn handle_active_pane_left_mouse_press( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + let floating_panes_are_visible = self.floating_panes.panes_are_visible(); + let pane_at_position = self + .get_pane_at(&event.position, false) .with_context(err_context)? - .map(|pane| pane.intercept_left_mouse_click(&position, client_id)) - .unwrap_or(false); - if intercepted { - self.set_force_render(); - return Ok(()); + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + if pane_at_position.position_is_on_frame(&event.position) { + // intercept frame click eg. for toggling pinned + let intercepted = pane_at_position.intercept_mouse_event_on_frame(&event, client_id); + if intercepted { + self.set_force_render(); + return Ok(true); + } else if floating_panes_are_visible { + // start moving if floating pane + let search_selectable = false; + if self + .floating_panes + .move_pane_with_mouse(event.position, search_selectable) + { + self.swap_layouts.set_is_floating_damaged(); + self.set_force_render(); + return Ok(true); + } + } + } else { + let relative_position = pane_at_position.relative_position(&event.position); + if let Some(mouse_event) = pane_at_position.mouse_left_click(&relative_position, false) + { + // send click to terminal if needed (eg. the program inside + // requested mouse mode) + if !pane_at_position.position_is_on_frame(&event.position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } + } else { + // start selection for copy/paste + pane_at_position.start_selection(&relative_position, client_id); + if let PaneId::Terminal(_) = pane_at_position.pid() { + self.selecting_with_mouse_in_pane = Some(pane_at_position.pid()); + } + } } - + Ok(false) + } + fn handle_inactive_pane_left_mouse_press( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); if !self.floating_panes.panes_are_visible() { let search_selectable = false; if let Ok(Some(pane_id)) = self .floating_panes - .get_pinned_pane_id_at(position, search_selectable) + .get_pinned_pane_id_at(&event.position, search_selectable) { // here, the floating panes are not visible, but there is a pinned pane (always // visible) that has been clicked on - so we make the entire surface visible and // focus it self.show_floating_panes(); self.floating_panes.focus_pane(pane_id, client_id); - return Ok(()); + return Ok(true); } } - - self.focus_pane_at(position, client_id) + let active_pane_id_before_click = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + self.focus_pane_at(&event.position, client_id) .with_context(err_context)?; - let search_selectable = false; - if self.floating_panes.panes_are_visible() - && self + if let Some(pane_at_position) = self.unselectable_pane_at_position(&event.position) { + let relative_position = pane_at_position.relative_position(&event.position); + // we use start_selection here because it has a client_id, + // ideally we should add client_id to mouse_left_click and others, but this should be + // dealt with as part of the trait removal refactoring + pane_at_position.start_selection(&relative_position, client_id); + } + + if self.floating_panes.panes_are_visible() { + let search_selectable = false; + // we do this because this might be the beginning of the user dragging a pane + // that was not focused + // TODO: rename move_pane_with_mouse to "start_moving_pane_with_mouse"? + return Ok(self .floating_panes - .move_pane_with_mouse(*position, search_selectable) + .move_pane_with_mouse(event.position, search_selectable)); + } + let active_pane_id_after_click = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + if active_pane_id_before_click != active_pane_id_after_click { + // focus changed, need to report it + Ok(true) + } else { + Ok(false) + } + } + fn handle_left_mouse_motion( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + let pane_is_being_moved_with_mouse = self.floating_panes.pane_is_being_moved_with_mouse(); + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + if pane_is_being_moved_with_mouse { + let search_selectable = false; + if self + .floating_panes + .move_pane_with_mouse(event.position, search_selectable) + { + self.swap_layouts.set_is_floating_damaged(); + self.set_force_render(); + return Ok(true); + } + } else if let Some(pane_id_with_selection) = self.selecting_with_mouse_in_pane { + if let Some(pane_with_selection) = self.get_pane_with_id_mut(pane_id_with_selection) { + let relative_position = pane_with_selection.relative_position(&event.position); + pane_with_selection.update_selection(&relative_position, client_id); + } + } else { + let pane_at_position = self + .get_pane_at(&event.position, false) + .with_context(err_context)? + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; + if pane_at_position.pid() == active_pane_id { + self.write_mouse_event_to_active_pane(event, client_id)?; + } + } + Ok(false) + } + fn handle_left_mouse_release( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { + let err_context = + || format!("failed to handle mouse event {event:?} for client {client_id}"); + let floating_panes_are_visible = self.floating_panes.panes_are_visible(); + let copy_on_release = self.copy_on_select; + + if let Some(pane_with_selection) = self + .selecting_with_mouse_in_pane + .and_then(|p_id| self.get_pane_with_id_mut(p_id)) { - self.swap_layouts.set_is_floating_damaged(); - self.set_force_render(); - return Ok(()); + let mut relative_position = pane_with_selection.relative_position(&event.position); + + relative_position.change_column( + (relative_position.column()) + .max(0) + .min(pane_with_selection.get_content_columns()), + ); + + relative_position.change_line( + (relative_position.line()) + .max(0) + .min(pane_with_selection.get_content_rows() as isize), + ); + + if let Some(mouse_event) = + pane_with_selection.mouse_left_click_release(&relative_position) + { + self.write_to_active_terminal(&None, mouse_event.into_bytes(), false, client_id) + .with_context(err_context)?; + } else { + let relative_position = pane_with_selection.relative_position(&event.position); + pane_with_selection.end_selection(&relative_position, client_id); + if let PaneId::Terminal(_) = pane_with_selection.pid() { + if copy_on_release { + let selected_text = pane_with_selection.get_selected_text(); + pane_with_selection.reset_selection(); + + if let Some(selected_text) = selected_text { + self.write_selection_to_clipboard(&selected_text) + .with_context(err_context)?; + } + } + } + + self.selecting_with_mouse_in_pane = None; + } + } else if floating_panes_are_visible && self.floating_panes.pane_is_being_moved_with_mouse() + { + self.floating_panes + .stop_moving_pane_with_mouse(event.position); + } else { + self.write_mouse_event_to_active_pane(event, client_id)?; } + Ok(false) + } + + pub fn handle_right_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + let err_context = || format!("failed to handle mouse right click for client {client_id}"); + + let absolute_position = event.position; + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; if let Some(pane) = self - .get_pane_at(position, false) + .get_pane_at(&absolute_position, false) .with_context(err_context)? { - let relative_position = pane.relative_position(position); - if let Some(mouse_event) = pane.mouse_left_click(&relative_position, false) { - if !pane.position_is_on_frame(position) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; - } - } else { - pane.start_selection(&relative_position, client_id); - if let PaneId::Terminal(_) = pane.pid() { - self.selecting_with_mouse = true; + if pane.pid() == active_pane_id { + let relative_position = pane.relative_position(&absolute_position); + let mut event_for_pane = event.clone(); + event_for_pane.position = relative_position; + if let Some(mouse_event) = pane.mouse_event(&event_for_pane) { + if !pane.position_is_on_frame(&absolute_position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } + } else { + pane.handle_right_click(&relative_position, client_id); } } }; - Ok(()) + Ok(false) } - pub fn handle_right_click(&mut self, position: &Position, client_id: ClientId) -> Result<()> { - let err_context = || { - format!( - "failed to handle mouse right click at position {position:?} for client {client_id}" - ) - }; + fn handle_middle_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + let err_context = || format!("failed to handle mouse middle click for client {client_id}"); + let absolute_position = event.position; - self.focus_pane_at(position, client_id) - .with_context(err_context)?; + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; if let Some(pane) = self - .get_pane_at(position, false) + .get_pane_at(&absolute_position, false) .with_context(err_context)? { - let relative_position = pane.relative_position(position); - if let Some(mouse_event) = pane.mouse_right_click(&relative_position, false) { - if !pane.position_is_on_frame(position) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; + if pane.pid() == active_pane_id { + let relative_position = pane.relative_position(&absolute_position); + let mut event_for_pane = event.clone(); + event_for_pane.position = relative_position; + if let Some(mouse_event) = pane.mouse_event(&event_for_pane) { + if !pane.position_is_on_frame(&absolute_position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } } - } else { - pane.handle_right_click(&relative_position, client_id); } }; - Ok(()) + Ok(false) } - pub fn handle_middle_click(&mut self, position: &Position, client_id: ClientId) -> Result<()> { - let err_context = || { - format!( - "failed to handle mouse middle click at position {position:?} for client {client_id}" - ) - }; + fn handle_mouse_no_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + let err_context = || format!("failed to handle mouse no click for client {client_id}"); + let absolute_position = event.position; - self.focus_pane_at(position, client_id) - .with_context(err_context)?; + let active_pane_id = self + .get_active_pane_id(client_id) + .ok_or_else(|| anyhow!("Failed to find pane at position"))?; if let Some(pane) = self - .get_pane_at(position, false) + .get_pane_at(&absolute_position, false) .with_context(err_context)? { - let relative_position = pane.relative_position(position); - if let Some(mouse_event) = pane.mouse_middle_click(&relative_position, false) { - if !pane.position_is_on_frame(position) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; + if pane.pid() == active_pane_id { + let relative_position = pane.relative_position(&absolute_position); + let mut event_for_pane = event.clone(); + event_for_pane.position = relative_position; + if let Some(mouse_event) = pane.mouse_event(&event_for_pane) { + if !pane.position_is_on_frame(&absolute_position) { + self.write_to_active_terminal( + &None, + mouse_event.into_bytes(), + false, + client_id, + ) + .with_context(err_context)?; + } } } }; - Ok(()) + Ok(false) } + fn unselectable_pane_at_position(&mut self, point: &Position) -> Option<&mut Box> { + // the repetition in this function is to appease the borrow checker, I don't like it either + let floating_panes_are_visible = self.floating_panes.panes_are_visible(); + if floating_panes_are_visible { + if let Ok(Some(clicked_pane_id)) = self.floating_panes.get_pane_id_at(point, true) { + if let Some(pane) = self.floating_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } else if let Ok(Some(clicked_pane_id)) = self.get_pane_id_at(point, false) { + if let Some(pane) = self.tiled_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } + } else if let Ok(Some(clicked_pane_id)) = self.get_pane_id_at(point, false) { + if let Some(pane) = self.tiled_panes.get_pane_mut(clicked_pane_id) { + if !pane.selectable() { + return Some(pane); + } + } + } + None + } fn focus_pane_at(&mut self, point: &Position, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to focus pane at position {point:?} for client {client_id}"); @@ -3489,7 +3767,7 @@ impl Tab { Ok(()) } - pub fn handle_middle_mouse_release( + fn handle_middle_mouse_release( &mut self, position: &Position, client_id: ClientId, @@ -3521,239 +3799,6 @@ impl Tab { } Ok(()) } - - pub fn handle_left_mouse_release( - &mut self, - position: &Position, - client_id: ClientId, - ) -> Result<()> { - let err_context = || { - format!("failed to handle left mouse release at position {position:?} for client {client_id}") - }; - - self.last_mouse_hold_position = None; - - if self.floating_panes.panes_are_visible() - && self.floating_panes.pane_is_being_moved_with_mouse() - { - self.floating_panes.stop_moving_pane_with_mouse(*position); - return Ok(()); - } - - // read these here to avoid use of borrowed `*self`, since we are holding active_pane - let selecting = self.selecting_with_mouse; - let copy_on_release = self.copy_on_select; - let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); - - if let Some(active_pane) = active_pane { - let mut relative_position = active_pane.relative_position(position); - relative_position.change_column( - (relative_position.column()) - .max(0) - .min(active_pane.get_content_columns()), - ); - - relative_position.change_line( - (relative_position.line()) - .max(0) - .min(active_pane.get_content_rows() as isize), - ); - - if let Some(mouse_event) = active_pane.mouse_left_click_release(&relative_position) { - self.write_to_active_terminal(&None, mouse_event.into_bytes(), false, client_id) - .with_context(err_context)?; - } else { - let relative_position = active_pane.relative_position(position); - if let PaneId::Terminal(_) = active_pane.pid() { - if selecting { - active_pane.end_selection(&relative_position, client_id); - if copy_on_release { - let selected_text = active_pane.get_selected_text(); - active_pane.reset_selection(); - - if let Some(selected_text) = selected_text { - self.write_selection_to_clipboard(&selected_text) - .with_context(err_context)?; - } - } - } - } else { - // notify the release event to a plugin pane, should be renamed - active_pane.end_selection(&relative_position, client_id); - } - - self.selecting_with_mouse = false; - } - } - Ok(()) - } - - pub fn handle_mouse_hold_left( - &mut self, - position_on_screen: &Position, - client_id: ClientId, - ) -> Result { - let err_context = || { - format!("failed to handle left mouse hold at position {position_on_screen:?} for client {client_id}") - }; - - // return value indicates whether we should trigger a render - // determine if event is repeated to enable smooth scrolling - let is_repeated = if let Some(last_position) = self.last_mouse_hold_position { - position_on_screen == &last_position - } else { - false - }; - self.last_mouse_hold_position = Some(*position_on_screen); - - let search_selectable = true; - - if self.floating_panes.panes_are_visible() - && self.floating_panes.pane_is_being_moved_with_mouse() - && self - .floating_panes - .move_pane_with_mouse(*position_on_screen, search_selectable) - { - self.swap_layouts.set_is_floating_damaged(); - self.set_force_render(); - return Ok(!is_repeated); // we don't need to re-render in this case if the pane did not move - // return; - } - - let selecting = self.selecting_with_mouse; - let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); - - if let Some(active_pane) = active_pane { - let mut relative_position = active_pane.relative_position(position_on_screen); - if !is_repeated { - // ensure that coordinates are valid - relative_position.change_column( - (relative_position.column()) - .max(0) - .min(active_pane.get_content_columns()), - ); - - relative_position.change_line( - (relative_position.line()) - .max(0) - .min(active_pane.get_content_rows() as isize), - ); - if let Some(mouse_event) = active_pane.mouse_left_click(&relative_position, true) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; - return Ok(true); // we need to re-render in this case so the selection disappears - } - } else if selecting { - active_pane.update_selection(&relative_position, client_id); - return Ok(true); // we need to re-render in this case so the selection is updated - } - } - Ok(false) // we shouldn't even get here, but might as well not needlessly render if we do - } - - pub fn handle_mouse_hold_right( - &mut self, - position_on_screen: &Position, - client_id: ClientId, - ) -> Result { - let err_context = || { - format!("failed to handle left mouse hold at position {position_on_screen:?} for client {client_id}") - }; - - // return value indicates whether we should trigger a render - // determine if event is repeated to enable smooth scrolling - let is_repeated = if let Some(last_position) = self.last_mouse_hold_position { - position_on_screen == &last_position - } else { - false - }; - self.last_mouse_hold_position = Some(*position_on_screen); - - let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); - - if let Some(active_pane) = active_pane { - let mut relative_position = active_pane.relative_position(position_on_screen); - if !is_repeated { - relative_position.change_column( - (relative_position.column()) - .max(0) - .min(active_pane.get_content_columns()), - ); - - relative_position.change_line( - (relative_position.line()) - .max(0) - .min(active_pane.get_content_rows() as isize), - ); - if let Some(mouse_event) = active_pane.mouse_right_click(&relative_position, true) { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; - return Ok(true); // we need to re-render in this case so the selection disappears - } - } - } - Ok(false) // we shouldn't even get here, but might as well not needlessly render if we do - } - - pub fn handle_mouse_hold_middle( - &mut self, - position_on_screen: &Position, - client_id: ClientId, - ) -> Result { - let err_context = || { - format!("failed to handle left mouse hold at position {position_on_screen:?} for client {client_id}") - }; - // return value indicates whether we should trigger a render - // determine if event is repeated to enable smooth scrolling - let is_repeated = if let Some(last_position) = self.last_mouse_hold_position { - position_on_screen == &last_position - } else { - false - }; - self.last_mouse_hold_position = Some(*position_on_screen); - - let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); - - if let Some(active_pane) = active_pane { - let mut relative_position = active_pane.relative_position(position_on_screen); - if !is_repeated { - relative_position.change_column( - (relative_position.column()) - .max(0) - .min(active_pane.get_content_columns()), - ); - - relative_position.change_line( - (relative_position.line()) - .max(0) - .min(active_pane.get_content_rows() as isize), - ); - if let Some(mouse_event) = active_pane.mouse_middle_click(&relative_position, true) - { - self.write_to_active_terminal( - &None, - mouse_event.into_bytes(), - false, - client_id, - ) - .with_context(err_context)?; - return Ok(true); // we need to re-render in this case so the selection disappears - } - } - } - Ok(false) // we shouldn't even get here, but might as well not needlessly render if we do - } - pub fn copy_selection(&self, client_id: ClientId) -> Result<()> { let selected_text = self .get_active_pane(client_id) diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 76b4474b29..74c2d4d99a 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -23,6 +23,7 @@ use zellij_utils::input::layout::{ FloatingPaneLayout, Layout, PluginUserConfiguration, RunPluginLocation, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }; +use zellij_utils::input::mouse::MouseEvent; use zellij_utils::input::plugins::PluginTag; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::{Size, SizeInPixels}; @@ -1728,10 +1729,16 @@ fn move_floating_pane_focus_with_mouse() { .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); - tab.handle_left_click(&Position::new(9, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(9, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(9, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(9, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), @@ -1826,10 +1833,16 @@ fn move_pane_focus_with_mouse_to_non_floating_pane() { .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); - tab.handle_left_click(&Position::new(4, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(4, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(4, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(4, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), @@ -1924,10 +1937,16 @@ fn drag_pane_with_mouse() { .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), @@ -2022,16 +2041,22 @@ fn mark_text_inside_floating_pane() { .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); - tab.handle_left_click(&Position::new(6, 30), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(6, 30)), + client_id, + ) + .unwrap(); assert!( - tab.selecting_with_mouse, + tab.selecting_with_mouse_in_pane.is_some(), "started selecting with mouse on click" ); - tab.handle_left_mouse_release(&Position::new(5, 15), client_id) - .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(5, 15)), + client_id, + ) + .unwrap(); assert!( - !tab.selecting_with_mouse, + tab.selecting_with_mouse_in_pane.is_none(), "stopped selecting with mouse on release" ); tab.render(&mut output).unwrap(); @@ -2630,10 +2655,16 @@ fn move_floating_pane_with_sixel_image() { .unwrap(); let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(2, fixture).unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let snapshot = take_snapshot_with_sixel( @@ -2668,10 +2699,16 @@ fn floating_pane_above_sixel_image() { .unwrap(); let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(1, fixture).unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let snapshot = take_snapshot_with_sixel( @@ -3048,22 +3085,43 @@ fn pane_in_sgr_button_event_tracking_mouse_mode() { let sgr_mouse_mode_any_button = String::from("\u{1b}[?1002;1006h"); // button event tracking (1002) with SGR encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_left(&Position::new(9, 72), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_right_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_right(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_middle_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) @@ -3110,22 +3168,43 @@ fn pane_in_sgr_normal_event_tracking_mouse_mode() { let sgr_mouse_mode_any_button = String::from("\u{1b}[?1000;1006h"); // normal event tracking (1000) with sgr encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_left(&Position::new(9, 72), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_right_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_right(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_middle_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) @@ -3154,7 +3233,7 @@ fn pane_in_sgr_normal_event_tracking_mouse_mode() { } #[test] -fn pane_in_utf8_button_event_tracking_mouse_mode() { +fn pane_in_sgr_any_event_tracking_mouse_mode() { let size = Size { cols: 121, rows: 20, @@ -3169,25 +3248,137 @@ fn pane_in_utf8_button_event_tracking_mouse_mode() { ); pty_instruction_bus.start(); - let sgr_mouse_mode_any_button = String::from("\u{1b}[?1002;1005h"); // button event tracking (1002) with utf8 encoding (1005) + let sgr_mouse_mode_any_button = String::from("\u{1b}[?1003;1006h"); // any event tracking (1003) with SGR encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) + // TODO: CONTINUE HERE - make sure these pass, then add some button-less motions and see what + // we track them + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_mouse_hold_left(&Position::new(9, 72), client_id) + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) + tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) .unwrap(); - tab.handle_right_click(&Position::new(5, 71), client_id) + tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) .unwrap(); - tab.handle_mouse_hold_right(&Position::new(9, 72), client_id) + tab.handle_mouse_event( + &MouseEvent::new_buttonless_motion(Position::new(9, 72)), + client_id, + ) + .unwrap(); + + pty_instruction_bus.exit(); + + assert_eq!( + pty_instruction_bus.clone_output(), + vec![ + "\u{1b}[<0;71;5M".to_string(), // SGR left click + "\u{1b}[<32;72;9M".to_string(), // SGR left click (hold) + "\u{1b}[<0;75;7m".to_string(), // SGR left button release + "\u{1b}[<2;71;5M".to_string(), // SGR right click + "\u{1b}[<34;72;9M".to_string(), // SGR right click (hold) + "\u{1b}[<2;75;7m".to_string(), // SGR right button release + "\u{1b}[<1;71;5M".to_string(), // SGR middle click + "\u{1b}[<33;72;9M".to_string(), // SGR middle click (hold) + "\u{1b}[<1;75;7m".to_string(), // SGR middle button release + "\u{1b}[<64;71;5M".to_string(), // SGR scroll up + "\u{1b}[<65;71;5M".to_string(), // SGR scroll down + "\u{1b}[<35;72;9M".to_string(), // SGR buttonless motion + ] + ); +} + +#[test] +fn pane_in_utf8_button_event_tracking_mouse_mode() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + + let mut pty_instruction_bus = MockPtyInstructionBus::new(); + let mut tab = create_new_tab_with_mock_pty_writer( + size, + ModeInfo::default(), + pty_instruction_bus.pty_write_sender(), + ); + pty_instruction_bus.start(); + + let sgr_mouse_mode_any_button = String::from("\u{1b}[?1002;1005h"); // button event tracking (1002) with utf8 encoding (1005) + tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_middle_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) @@ -3234,22 +3425,43 @@ fn pane_in_utf8_normal_event_tracking_mouse_mode() { let sgr_mouse_mode_any_button = String::from("\u{1b}[?1000;1005h"); // normal event tracking (1000) with sgr encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); - tab.handle_left_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_left(&Position::new(9, 72), client_id) - .unwrap(); - tab.handle_left_mouse_release(&Position::new(7, 75), client_id) - .unwrap(); - tab.handle_right_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_right(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_right_mouse_release(&Position::new(7, 75), client_id) .unwrap(); - tab.handle_middle_click(&Position::new(5, 71), client_id) - .unwrap(); - tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id) - .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) @@ -3277,6 +3489,95 @@ fn pane_in_utf8_normal_event_tracking_mouse_mode() { ); } +#[test] +fn pane_in_utf8_any_event_tracking_mouse_mode() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + + let mut pty_instruction_bus = MockPtyInstructionBus::new(); + let mut tab = create_new_tab_with_mock_pty_writer( + size, + ModeInfo::default(), + pty_instruction_bus.pty_write_sender(), + ); + pty_instruction_bus.start(); + + let sgr_mouse_mode_any_button = String::from("\u{1b}[?1003;1005h"); // any event tracking (1002) with utf8 encoding (1005) + tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_motion( + &MouseEvent::new_left_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_left_mouse_release( + &MouseEvent::new_left_release_event(Position::new(7, 75)), + client_id, + ) + .unwrap(); + tab.handle_right_click( + &MouseEvent::new_right_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_right_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_right_mouse_release(&Position::new(7, 75), client_id) + .unwrap(); + tab.handle_middle_click( + &MouseEvent::new_middle_press_event(Position::new(5, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_middle_motion_event(Position::new(9, 72)), + client_id, + ) + .unwrap(); + tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) + .unwrap(); + tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) + .unwrap(); + tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_buttonless_motion(Position::new(9, 72)), + client_id, + ) + .unwrap(); + + pty_instruction_bus.exit(); + + assert_eq!( + pty_instruction_bus.clone_output(), + vec![ + "\u{1b}[M g%".to_string(), // utf8 left click + "\u{1b}[M@h)".to_string(), // utf8 left click (hold) + "\u{1b}[M#k'".to_string(), // utf8 left button release + "\u{1b}[M\"g%".to_string(), // utf8 right click + "\u{1b}[MBh)".to_string(), // utf8 right click (hold) + "\u{1b}[M#k'".to_string(), // utf8 right button release + "\u{1b}[M!g%".to_string(), // utf8 middle click + "\u{1b}[MAh)".to_string(), // utf8 middle click (hold) + "\u{1b}[M#k'".to_string(), // utf8 middle click release + "\u{1b}[M`g%".to_string(), // utf8 scroll up + "\u{1b}[Mag%".to_string(), // utf8 scroll down + "\u{1b}[MCh)".to_string(), // urf8 buttonless motion + ] + ); +} + #[test] fn tab_with_basic_layout() { let layout = r#" @@ -5941,8 +6242,11 @@ fn focus_stacked_pane_over_flexible_pane_with_the_mouse() { Some(client_id), ) .unwrap(); - tab.handle_left_click(&Position::new(1, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(1, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), @@ -6040,10 +6344,16 @@ fn focus_stacked_pane_under_flexible_pane_with_the_mouse() { Some(client_id), ) .unwrap(); - tab.handle_left_click(&Position::new(1, 71), client_id) - .unwrap(); - tab.handle_left_click(&Position::new(9, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(1, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(9, 71)), + client_id, + ) + .unwrap(); tab.render(&mut output).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), @@ -6141,10 +6451,16 @@ fn close_stacked_pane_with_previously_focused_other_pane() { Some(client_id), ) .unwrap(); - tab.handle_left_click(&Position::new(2, 71), client_id) - .unwrap(); - tab.handle_left_click(&Position::new(1, 71), client_id) - .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(2, 71)), + client_id, + ) + .unwrap(); + tab.handle_mouse_event( + &MouseEvent::new_left_press_event(Position::new(1, 71)), + client_id, + ) + .unwrap(); tab.close_pane(PaneId::Terminal(4), false); tab.render(&mut output).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index 87ca37dd4c..a93dde4637 100644 --- a/zellij-utils/assets/prost/api.action.rs +++ b/zellij-utils/assets/prost/api.action.rs @@ -5,7 +5,7 @@ pub struct Action { pub name: i32, #[prost( oneof = "action::OptionalPayload", - tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48" + tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49" )] pub optional_payload: ::core::option::Option, } @@ -68,12 +68,6 @@ pub mod action { RightMouseReleasePayload(super::Position), #[prost(message, tag = "28")] MiddleMouseReleasePayload(super::Position), - #[prost(message, tag = "29")] - MouseHoldLeftPayload(super::Position), - #[prost(message, tag = "30")] - MouseHoldRightPayload(super::Position), - #[prost(message, tag = "31")] - MouseHoldMiddlePayload(super::Position), #[prost(bytes, tag = "32")] SearchInputPayload(::prost::alloc::vec::Vec), #[prost(enumeration = "super::SearchDirection", tag = "33")] @@ -108,6 +102,8 @@ pub mod action { MessagePayload(super::CliPipePayload), #[prost(enumeration = "super::MoveTabDirection", tag = "48")] MoveTabPayload(i32), + #[prost(message, tag = "49")] + MouseEventPayload(super::MouseEventPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -256,6 +252,32 @@ pub struct Position { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct MouseEventPayload { + #[prost(uint32, tag = "1")] + pub event_type: u32, + #[prost(bool, tag = "2")] + pub left: bool, + #[prost(bool, tag = "3")] + pub right: bool, + #[prost(bool, tag = "4")] + pub middle: bool, + #[prost(bool, tag = "5")] + pub wheel_up: bool, + #[prost(bool, tag = "6")] + pub wheel_down: bool, + #[prost(bool, tag = "7")] + pub shift: bool, + #[prost(bool, tag = "8")] + pub alt: bool, + #[prost(bool, tag = "9")] + pub ctrl: bool, + #[prost(int64, tag = "10")] + pub line: i64, + #[prost(int64, tag = "11")] + pub column: i64, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct RunCommandAction { #[prost(string, tag = "1")] pub command: ::prost::alloc::string::String, @@ -427,9 +449,6 @@ pub enum ActionName { LeftMouseRelease = 54, RightMouseRelease = 55, MiddleMouseRelease = 56, - MouseHoldLeft = 57, - MouseHoldRight = 58, - MouseHoldMiddle = 59, SearchInput = 60, Search = 61, SearchToggleOption = 62, @@ -456,6 +475,7 @@ pub enum ActionName { MoveTab = 83, KeybindPipe = 84, TogglePanePinned = 85, + MouseEvent = 86, } impl ActionName { /// String value of the enum field names used in the ProtoBuf definition. @@ -521,9 +541,6 @@ impl ActionName { ActionName::LeftMouseRelease => "LeftMouseRelease", ActionName::RightMouseRelease => "RightMouseRelease", ActionName::MiddleMouseRelease => "MiddleMouseRelease", - ActionName::MouseHoldLeft => "MouseHoldLeft", - ActionName::MouseHoldRight => "MouseHoldRight", - ActionName::MouseHoldMiddle => "MouseHoldMiddle", ActionName::SearchInput => "SearchInput", ActionName::Search => "Search", ActionName::SearchToggleOption => "SearchToggleOption", @@ -550,6 +567,7 @@ impl ActionName { ActionName::MoveTab => "MoveTab", ActionName::KeybindPipe => "KeybindPipe", ActionName::TogglePanePinned => "TogglePanePinned", + ActionName::MouseEvent => "MouseEvent", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -612,9 +630,6 @@ impl ActionName { "LeftMouseRelease" => Some(Self::LeftMouseRelease), "RightMouseRelease" => Some(Self::RightMouseRelease), "MiddleMouseRelease" => Some(Self::MiddleMouseRelease), - "MouseHoldLeft" => Some(Self::MouseHoldLeft), - "MouseHoldRight" => Some(Self::MouseHoldRight), - "MouseHoldMiddle" => Some(Self::MouseHoldMiddle), "SearchInput" => Some(Self::SearchInput), "Search" => Some(Self::Search), "SearchToggleOption" => Some(Self::SearchToggleOption), @@ -641,6 +656,7 @@ impl ActionName { "MoveTab" => Some(Self::MoveTab), "KeybindPipe" => Some(Self::KeybindPipe), "TogglePanePinned" => Some(Self::TogglePanePinned), + "MouseEvent" => Some(Self::MouseEvent), _ => None, } } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index bd82b61c94..b159a21ef5 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -306,9 +306,7 @@ pub enum ScreenContext { LeftMouseRelease, RightMouseRelease, MiddleMouseRelease, - MouseHoldLeft, - MouseHoldRight, - MouseHoldMiddle, + MouseEvent, Copy, ToggleTab, AddClient, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 95ec6eb26b..615a7f5e60 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -10,6 +10,7 @@ use crate::data::{Direction, KeyWithModifier, PaneId, Resize}; use crate::data::{FloatingPaneCoordinates, InputMode}; use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; +use crate::input::mouse::{MouseEvent, MouseEventType}; use crate::input::options::OnForceClose; use miette::{NamedSource, Report}; use serde::{Deserialize, Serialize}; @@ -215,19 +216,11 @@ pub enum Action { Run(RunCommandAction), /// Detach session and exit Detach, - LeftClick(Position), - RightClick(Position), - MiddleClick(Position), LaunchOrFocusPlugin(RunPluginOrAlias, bool, bool, bool, bool), // bools => should float, // move_to_focused_tab, should_open_in_place, skip_cache LaunchPlugin(RunPluginOrAlias, bool, bool, bool, Option), // bools => should float, // should_open_in_place, skip_cache, Option is cwd - LeftMouseRelease(Position), - RightMouseRelease(Position), - MiddleMouseRelease(Position), - MouseHoldLeft(Position), - MouseHoldRight(Position), - MouseHoldMiddle(Position), + MouseEvent(MouseEvent), Copy, /// Confirm a prompt Confirm, @@ -803,6 +796,14 @@ impl Action { _ => false, } } + pub fn is_mouse_motion(&self) -> bool { + if let Action::MouseEvent(mouse_event) = self { + if let MouseEventType::Motion = mouse_event.event_type { + return true; + } + } + false + } } impl From for Action { diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 296b531380..aaf466d6ac 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -3,15 +3,12 @@ pub mod command; pub mod config; pub mod keybinds; pub mod layout; +pub mod mouse; pub mod options; pub mod permission; pub mod plugins; pub mod theme; -// Can't use this in wasm due to dependency on the `termwiz` crate. -#[cfg(not(target_family = "wasm"))] -pub mod mouse; - #[cfg(not(target_family = "wasm"))] pub use not_wasm::*; diff --git a/zellij-utils/src/input/mouse.rs b/zellij-utils/src/input/mouse.rs index 0a63ac2df0..4f89cce873 100644 --- a/zellij-utils/src/input/mouse.rs +++ b/zellij-utils/src/input/mouse.rs @@ -2,102 +2,210 @@ use serde::{Deserialize, Serialize}; use crate::position::Position; +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +/// A mouse event can have any number of buttons (including no +/// buttons) pressed or released. +pub struct MouseEvent { + /// A mouse event can current be a Press, Release, or Motion. + /// Future events could consider double-click and triple-click. + pub event_type: MouseEventType, + + // Mouse buttons associated with this event. + pub left: bool, + pub right: bool, + pub middle: bool, + pub wheel_up: bool, + pub wheel_down: bool, + + // Keyboard modifier flags can be encoded with events too. They + // are not often passed on the wire (instead used for + // selection/copy-paste and changing terminal properties + // on-the-fly at the user-facing terminal), but alt-mouseclick + // usually passes through and is testable on vttest. termwiz + // already exposes them too. + pub shift: bool, + pub alt: bool, + pub ctrl: bool, + + /// The coordinates are zero-based. + pub position: Position, +} + /// A mouse related event -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub enum MouseEvent { +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)] +pub enum MouseEventType { /// A mouse button was pressed. - /// - /// The coordinates are zero-based. - Press(MouseButton, Position), + Press, /// A mouse button was released. - /// - /// The coordinates are zero-based. - Release(Position), + Release, /// A mouse button is held over the given coordinates. - /// - /// The coordinates are zero-based. - Hold(Position), + Motion, } -impl From for MouseEvent { - fn from(event: termwiz::input::MouseEvent) -> Self { - #[allow(clippy::if_same_then_else)] - if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::LEFT) - { - MouseEvent::Press( - MouseButton::Left, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } else if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::RIGHT) - { - MouseEvent::Press( - MouseButton::Right, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } else if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::MIDDLE) - { - MouseEvent::Press( - MouseButton::Middle, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } else if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::VERT_WHEEL) - { - if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::WHEEL_POSITIVE) - { - MouseEvent::Press( - MouseButton::WheelUp, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } else { - MouseEvent::Press( - MouseButton::WheelDown, - Position::new(event.y.saturating_sub(1) as i32, event.x.saturating_sub(1)), - ) - } - } else if event - .mouse_buttons - .contains(termwiz::input::MouseButtons::NONE) - { - // release - MouseEvent::Release(Position::new( - event.y.saturating_sub(1) as i32, - event.x.saturating_sub(1), - )) - } else { - // this is an unsupported event, we just do this in order to send "something", but if - // something happens here, we might want to add more specific support - MouseEvent::Release(Position::new( - event.y.saturating_sub(1) as i32, - event.x.saturating_sub(1), - )) - } +impl MouseEvent { + pub fn new() -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: false, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position: Position::new(0, 0), + }; + event + } + pub fn new_buttonless_motion(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: false, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_left_press_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Press, + left: true, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_right_press_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Press, + left: false, + right: true, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_middle_press_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Press, + left: false, + right: false, + middle: true, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_middle_release_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Release, + left: false, + right: false, + middle: true, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_left_release_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Release, + left: true, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_left_motion_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: true, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_right_release_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Release, + left: false, + right: true, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_right_motion_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: false, + right: true, + middle: false, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event + } + pub fn new_middle_motion_event(position: Position) -> Self { + let event = MouseEvent { + event_type: MouseEventType::Motion, + left: false, + right: false, + middle: true, + wheel_up: false, + wheel_down: false, + shift: false, + alt: false, + ctrl: false, + position, + }; + event } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] -pub enum MouseButton { - /// The left mouse button. - Left, - /// The right mouse button. - Right, - /// The middle mouse button. - Middle, - /// Mouse wheel is going up. - /// - /// This event is typically only used with Mouse::Press. - WheelUp, - /// Mouse wheel is going down. - /// - /// This event is typically only used with Mouse::Press. - WheelDown, } diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index df7f3b05b7..3c4948cf2c 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -35,9 +35,6 @@ message Action { Position left_mouse_release_payload = 26; Position right_mouse_release_payload = 27; Position middle_mouse_release_payload = 28; - Position mouse_hold_left_payload = 29; - Position mouse_hold_right_payload = 30; - Position mouse_hold_middle_payload = 31; bytes search_input_payload = 32; SearchDirection search_payload = 33; SearchOption search_toggle_option_payload = 34; @@ -55,6 +52,7 @@ message Action { LaunchOrFocusPluginPayload launch_plugin_payload = 46; CliPipePayload message_payload = 47; MoveTabDirection move_tab_payload = 48; + MouseEventPayload mouse_event_payload = 49; } } @@ -216,9 +214,6 @@ enum ActionName { LeftMouseRelease = 54; RightMouseRelease = 55; MiddleMouseRelease = 56; - MouseHoldLeft = 57; - MouseHoldRight = 58; - MouseHoldMiddle = 59; SearchInput = 60; Search = 61; SearchToggleOption = 62; @@ -245,6 +240,7 @@ enum ActionName { MoveTab = 83; KeybindPipe = 84; TogglePanePinned = 85; + MouseEvent = 86; } message Position { @@ -252,6 +248,20 @@ message Position { int64 column = 2; } +message MouseEventPayload { + uint32 event_type = 1; + bool left = 2; + bool right = 3; + bool middle = 4; + bool wheel_up = 5; + bool wheel_down = 6; + bool shift = 7; + bool alt = 8; + bool ctrl = 9; + int64 line = 10; + int64 column = 11; +} + message RunCommandAction { string command = 1; repeated string args = 2; diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index e3c0513007..7330c656be 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -2,7 +2,8 @@ pub use super::generated_api::api::{ action::{ action::OptionalPayload, Action as ProtobufAction, ActionName as ProtobufActionName, DumpScreenPayload, EditFilePayload, GoToTabNamePayload, IdAndName, - LaunchOrFocusPluginPayload, MovePanePayload, MoveTabDirection as ProtobufMoveTabDirection, + LaunchOrFocusPluginPayload, MouseEventPayload as ProtobufMouseEventPayload, + MovePanePayload, MoveTabDirection as ProtobufMoveTabDirection, NameAndValue as ProtobufNameAndValue, NewFloatingPanePayload, NewPanePayload, NewPluginPanePayload, NewTiledPanePayload, PaneIdAndShouldFloat, PluginConfiguration as ProtobufPluginConfiguration, Position as ProtobufPosition, @@ -21,6 +22,7 @@ use crate::input::command::{OpenFilePayload, RunCommandAction}; use crate::input::layout::{ PluginUserConfiguration, RunPlugin, RunPluginLocation, RunPluginOrAlias, }; +use crate::input::mouse::{MouseEvent, MouseEventType}; use crate::position::Position; use std::collections::BTreeMap; @@ -382,21 +384,27 @@ impl TryFrom for Action { Some(ProtobufActionName::LeftClick) => match protobuf_action.optional_payload { Some(OptionalPayload::LeftClickPayload(payload)) => { let position = payload.try_into()?; - Ok(Action::LeftClick(position)) + Ok(Action::MouseEvent(MouseEvent::new_left_press_event( + position, + ))) }, _ => Err("Wrong payload for Action::LeftClick"), }, Some(ProtobufActionName::RightClick) => match protobuf_action.optional_payload { Some(OptionalPayload::RightClickPayload(payload)) => { let position = payload.try_into()?; - Ok(Action::RightClick(position)) + Ok(Action::MouseEvent(MouseEvent::new_right_press_event( + position, + ))) }, _ => Err("Wrong payload for Action::RightClick"), }, Some(ProtobufActionName::MiddleClick) => match protobuf_action.optional_payload { Some(OptionalPayload::MiddleClickPayload(payload)) => { let position = payload.try_into()?; - Ok(Action::MiddleClick(position)) + Ok(Action::MouseEvent(MouseEvent::new_middle_press_event( + position, + ))) }, _ => Err("Wrong payload for Action::MiddleClick"), }, @@ -460,14 +468,18 @@ impl TryFrom for Action { Some(ProtobufActionName::LeftMouseRelease) => match protobuf_action.optional_payload { Some(OptionalPayload::LeftMouseReleasePayload(payload)) => { let position = payload.try_into()?; - Ok(Action::LeftMouseRelease(position)) + Ok(Action::MouseEvent(MouseEvent::new_left_release_event( + position, + ))) }, _ => Err("Wrong payload for Action::LeftMouseRelease"), }, Some(ProtobufActionName::RightMouseRelease) => match protobuf_action.optional_payload { Some(OptionalPayload::RightMouseReleasePayload(payload)) => { let position = payload.try_into()?; - Ok(Action::RightMouseRelease(position)) + Ok(Action::MouseEvent(MouseEvent::new_right_release_event( + position, + ))) }, _ => Err("Wrong payload for Action::RightMouseRelease"), }, @@ -475,31 +487,19 @@ impl TryFrom for Action { match protobuf_action.optional_payload { Some(OptionalPayload::MiddleMouseReleasePayload(payload)) => { let position = payload.try_into()?; - Ok(Action::MiddleMouseRelease(position)) + Ok(Action::MouseEvent(MouseEvent::new_middle_release_event( + position, + ))) }, _ => Err("Wrong payload for Action::MiddleMouseRelease"), } }, - Some(ProtobufActionName::MouseHoldLeft) => match protobuf_action.optional_payload { - Some(OptionalPayload::MouseHoldLeftPayload(payload)) => { - let position = payload.try_into()?; - Ok(Action::MouseHoldLeft(position)) - }, - _ => Err("Wrong payload for Action::MouseHoldLeft"), - }, - Some(ProtobufActionName::MouseHoldRight) => match protobuf_action.optional_payload { - Some(OptionalPayload::MouseHoldRightPayload(payload)) => { - let position = payload.try_into()?; - Ok(Action::MouseHoldRight(position)) - }, - _ => Err("Wrong payload for Action::MouseHoldRight"), - }, - Some(ProtobufActionName::MouseHoldMiddle) => match protobuf_action.optional_payload { - Some(OptionalPayload::MouseHoldMiddlePayload(payload)) => { - let position = payload.try_into()?; - Ok(Action::MouseHoldMiddle(position)) + Some(ProtobufActionName::MouseEvent) => match protobuf_action.optional_payload { + Some(OptionalPayload::MouseEventPayload(payload)) => { + let event = payload.try_into()?; + Ok(Action::MouseEvent(event)) }, - _ => Err("Wrong payload for Action::MouseHoldMiddle"), + _ => Err("Wrong payload for Action::MouseEvent"), }, Some(ProtobufActionName::SearchInput) => match protobuf_action.optional_payload { Some(OptionalPayload::SearchInputPayload(payload)) => { @@ -1040,27 +1040,6 @@ impl TryFrom for ProtobufAction { name: ProtobufActionName::Detach as i32, optional_payload: None, }), - Action::LeftClick(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::LeftClick as i32, - optional_payload: Some(OptionalPayload::LeftClickPayload(position)), - }) - }, - Action::RightClick(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::RightClick as i32, - optional_payload: Some(OptionalPayload::RightClickPayload(position)), - }) - }, - Action::MiddleClick(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::MiddleClick as i32, - optional_payload: Some(OptionalPayload::MiddleClickPayload(position)), - }) - }, Action::LaunchOrFocusPlugin( run_plugin_or_alias, should_float, @@ -1105,46 +1084,11 @@ impl TryFrom for ProtobufAction { )), }) }, - Action::LeftMouseRelease(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::LeftMouseRelease as i32, - optional_payload: Some(OptionalPayload::LeftMouseReleasePayload(position)), - }) - }, - Action::RightMouseRelease(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::RightMouseRelease as i32, - optional_payload: Some(OptionalPayload::RightMouseReleasePayload(position)), - }) - }, - Action::MiddleMouseRelease(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::MiddleMouseRelease as i32, - optional_payload: Some(OptionalPayload::MiddleMouseReleasePayload(position)), - }) - }, - Action::MouseHoldLeft(position) => { - let position: ProtobufPosition = position.try_into()?; + Action::MouseEvent(event) => { + let payload: ProtobufMouseEventPayload = event.try_into()?; Ok(ProtobufAction { - name: ProtobufActionName::MouseHoldLeft as i32, - optional_payload: Some(OptionalPayload::MouseHoldLeftPayload(position)), - }) - }, - Action::MouseHoldRight(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::MouseHoldRight as i32, - optional_payload: Some(OptionalPayload::MouseHoldRightPayload(position)), - }) - }, - Action::MouseHoldMiddle(position) => { - let position: ProtobufPosition = position.try_into()?; - Ok(ProtobufAction { - name: ProtobufActionName::MouseHoldMiddle as i32, - optional_payload: Some(OptionalPayload::MouseHoldMiddlePayload(position)), + name: ProtobufActionName::MouseEvent as i32, + optional_payload: Some(OptionalPayload::MouseEventPayload(payload)), }) }, Action::SearchInput(bytes) => Ok(ProtobufAction { @@ -1442,6 +1386,51 @@ impl TryFrom for ProtobufPosition { } } +impl TryFrom for MouseEvent { + type Error = &'static str; + fn try_from(protobuf_event: ProtobufMouseEventPayload) -> Result { + Ok(MouseEvent { + event_type: match protobuf_event.event_type as u32 { + 0 => MouseEventType::Press, + 1 => MouseEventType::Release, + _ => MouseEventType::Motion, + }, + left: protobuf_event.left as bool, + right: protobuf_event.right as bool, + middle: protobuf_event.middle as bool, + wheel_up: protobuf_event.wheel_up as bool, + wheel_down: protobuf_event.wheel_down as bool, + shift: protobuf_event.shift as bool, + alt: protobuf_event.alt as bool, + ctrl: protobuf_event.ctrl as bool, + position: Position::new(protobuf_event.line as i32, protobuf_event.column as u16), + }) + } +} + +impl TryFrom for ProtobufMouseEventPayload { + type Error = &'static str; + fn try_from(event: MouseEvent) -> Result { + Ok(ProtobufMouseEventPayload { + event_type: match event.event_type { + MouseEventType::Press => 0, + MouseEventType::Release => 1, + MouseEventType::Motion => 2, + } as u32, + left: event.left as bool, + right: event.right as bool, + middle: event.middle as bool, + wheel_up: event.wheel_up as bool, + wheel_down: event.wheel_down as bool, + shift: event.shift as bool, + alt: event.alt as bool, + ctrl: event.ctrl as bool, + line: event.position.line.0 as i64, + column: event.position.column.0 as i64, + }) + } +} + impl TryFrom for PluginUserConfiguration { type Error = &'static str; fn try_from(plugin_configuration: ProtobufPluginConfiguration) -> Result {