diff options
author | Thomas Linford <tlinford@users.noreply.github.com> | 2021-07-02 16:40:50 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-02 16:40:50 +0200 |
commit | f93308f211565fae527f9b1e5788e12d0ea887c4 (patch) | |
tree | edb915be32a81c1caae2c7218398c96344a29bd0 | |
parent | 2a024db839e3be60f9428d39f6e84c7e748162d0 (diff) |
feat(ui): initial mouse support (#448)
* Initial mouse support
* enable mouse support (termion MouseTerminal)
* handle scroll up and down events
* Allow enabling/disabling of mouse reporting
Store the mouse terminal on OsInputOutput
* WIP: switch pane focus with mouse
* testing to get mouse character selection
* wip mouse selection
* wip: mouse selection
- initial handling of mouse events for
text selection within a pane
- wide characters currently problematic
- selection is not highlighted
* highlight currently selected text
* improve get currently selected text from TerminalPane
* copy selection to clipboard (wayland only for now)
* Add missing set_should_render on selection update
* Improve text selection
- Strip whitespace from end of lines
- Insert newlines when selection spans multiple lines
* Simplify selection logic
- Remove Range struct
- Selection is not an Option anymore, it's empty when start == end
* copy selection to clipboard with OSC-52 sequence
* Improve text selection with multiple panes
- Constrain mouse hold and release events to currently active pane
- Fix calculation of columns with side by side panes
* fix for PositionAndSize changes
* Fix mouse selection with full screen pane.
- Transform mouse event positions to relative when passing to pane.
* Move selection to grid, rework highlighting.
- Mark selected lines for update in grid output buffer, for now in the
simplest way possible, but can be made more efficient by calculating
changed lines only.
- Clear selection on pane resize.
- Re-add logic to forward mouse hold and release events only to
currently active pane.
* Tidy up selection
- add method to get selection line range
- add method to sort the current selection
* Grid: move current selection up/down when scrolling
- Make the selection work with lines in lines_above and lines_below
- Todo: update selection end when scrolling with mouse button being held
- Todo: figure out how to move selection when new characters are added.
* Grid: move selection when new lines are added
* Keep track of selection being active
- Handle the selection growing/shrinking when scrolling with mouse
button held down but not yet released.
* Improve selection end with multiple panes
* Tidying up
- remove logging statements, unused code
* Add some unit tests for selection, move position to zellij-utils
* Change Position::new to take i32 for line
* Grid: add unit tests for copy from selection
* add basic integration test for mouse pane focus
* Add basic integration test for mouse scroll
* Use color from palette for selection rendering
* Improve performance of selection render
- Try to minimize lines to update on selection start/update/end
* fixes for updated start method
* fix lines not in viewport being marked for rendering
- the previous optimization to grid selection rendering was always
adding the lines at the extremes of previous selection, causing problems
when scrolling up or down
- make sure to only add lines in viewport
* Disable mouse mode on exit
* use saturating_sub for usize subtractions
* copy selection to clipboard on mouse release
* disable mouse on exit after error
* remove left-over comments
* remove copy keybinding
* add default impl for selection methods in Pane
- remove the useless methods from Impl Pane in PluginPane
* move line diff between selections to selection
* add option to disable mouse mode
* Allow scrolling with mouse while selecting.
* move action repeater to os_input_output, change timeout to 10ms
- change repeater to take an action instead of a position with hardcoded
action
* replace mouse mode integration tests with e2e tests
* cleanup
* cleanup
* check if mouse mode is disabled from merged options
* fix missing changes in tests, cleanup
28 files changed, 1309 insertions, 17 deletions
diff --git a/Cargo.lock b/Cargo.lock index 264fb743c..df0db7317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,12 +190,12 @@ checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" [[package]] name = "atty" -version = "0.2.14" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" dependencies = [ - "hermit-abi", "libc", + "termion", "winapi", ] @@ -221,6 +221,12 @@ dependencies = [ ] [[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] name = "bincode" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2457,6 +2463,7 @@ name = "zellij-client" version = "0.14.0" dependencies = [ "insta", + "mio", "termbg", "zellij-utils", ] @@ -2467,6 +2474,7 @@ version = "0.14.0" dependencies = [ "ansi_term 0.12.1", "async-trait", + "base64", "cassowary", "daemonize", "insta", diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 69233f25a..8d41d44f9 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -1,7 +1,7 @@ #![allow(unused)] use ::insta::assert_snapshot; -use zellij_utils::pane_size::PositionAndSize; +use zellij_utils::{pane_size::PositionAndSize, position::Position}; use rand::Rng; @@ -54,6 +54,17 @@ pub const BRACKETED_PASTE_START: [u8; 6] = [27, 91, 50, 48, 48, 126]; // \u{1b}[ pub const BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201 pub const SLEEP: [u8; 0] = []; +// simplified, slighty adapted version of alacritty mouse reporting code +pub fn normal_mouse_report(position: Position, button: u8) -> Vec<u8> { + let Position { line, column } = position; + + let mut command = vec![b'\x1b', b'[', b'M', 32 + button]; + command.push(32 + 1 + column.0 as u8); + command.push(32 + 1 + line.0 as u8); + + command +} + // All the E2E tests are marked as "ignored" so that they can be run separately from the normal // tests @@ -785,3 +796,144 @@ pub fn accepts_basic_layout() { .run_all_steps(); assert_snapshot!(last_snapshot); } + +#[test] +#[ignore] +fn focus_pane_with_mouse() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + + let last_snapshot = RemoteRunner::new("split_terminals_vertically", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Click left pane", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + remote_terminal.send_key(&normal_mouse_report(Position::new(5, 2), 0)); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for left pane to be focused", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(2, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn scrolling_inside_a_pane_with_mouse() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = + RemoteRunner::new("scrolling_inside_a_pane_with_mouse", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() + && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Fill terminal with text", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + remote_terminal.send_key(&format!("{:0<57}", "line1 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line2 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line3 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line4 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line5 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line6 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line7 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line8 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line9 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line10 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line11 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line12 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line13 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line14 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line15 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line16 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line17 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line18 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line19 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<58}", "line20 ").as_bytes()); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Scroll up inside pane", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(119, 20) { + // all lines have been written to the pane + remote_terminal.send_key(&normal_mouse_report(Position::new(2, 64), 64)); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for scroll to finish", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(119, 20) + && remote_terminal.snapshot_contains("line1 ") + { + // scrolled up one line + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_pane_with_mouse.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_pane_with_mouse.snap new file mode 100644 index 000000000..79e84810d --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__focus_pane_with_mouse.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: last_snapshot + +--- + Zellij Tab #1 + +$ █ │$ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + + Ctrl + <g> LOCK <p> PANE <t> TAB <r> RESIZE <s> SCROLL <o> SESSION <q> QUIT + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane_with_mouse.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane_with_mouse.snap new file mode 100644 index 000000000..bff403764 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane_with_mouse.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: last_snapshot + +--- + Zellij Tab #1 + +$ │$ line1 000000000000000000000000000000000000000000000000000 + │line2 00000000000000000000000000000000000000000000000000000 + │line3 00000000000000000000000000000000000000000000000000000 + │line4 00000000000000000000000000000000000000000000000000000 + │line5 00000000000000000000000000000000000000000000000000000 + │line6 00000000000000000000000000000000000000000000000000000 + │line7 00000000000000000000000000000000000000000000000000000 + │line8 00000000000000000000000000000000000000000000000000000 + │line9 00000000000000000000000000000000000000000000000000000 + │line10 0000000000000000000000000000000000000000000000000000 + │line11 0000000000000000000000000000000000000000000000000000 + │line12 0000000000000000000000000000000000000000000000000000 + │line13 0000000000000000000000000000000000000000000000000000 + │line14 0000000000000000000000000000000000000000000000000000 + │line15 0000000000000000000000000000000000000000000000000000 + │line16 0000000000000000000000000000000000000000000000000000 + │line17 0000000000000000000000000000000000000000000000000000 + │line18 0000000000000000000000000000000000000000000000000000 + │line19 000000000000000000000000000000000000000000000000000█ + + Ctrl + <g> LOCK <p> PANE <t> TAB <r> RESIZE <s> SCROLL <o> SESSION <q> QUIT + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/fixtures/grid_copy b/src/tests/fixtures/grid_copy new file mode 100755 index 000000000..6d093741a --- /dev/null +++ b/src/tests/fixtures/grid_copy @@ -0,0 +1,59 @@ +[2m⏎(B[m
⏎
[KWelcome to fish, the friendly interactive shell
+Type `help` for instructions on how to use fish
+[?2004h]0;fish /home/thomas/Projects/zellij[30m(B[m[J[K
+[1;36mzellij[0m on [1;35m mouse-support[0m [1;31m[?][0m is [1;38;5;208m📦 v0.14.0[0m via [1;31m🦀 v1.53.0-beta.3 [0m[K
+[1;32m❯[0m [K
[C[Cc[J[38;2;255;0;0mc[30m(B[m[38;2;85;85;85mat test-input.txt[17D[30m(B[m[38;2;255;0;0ma[38;2;85;85;85mt test-input.txt[16D[30m(B[m[38;2;255;0;0mt[38;2;85;85;85m test-input.txt[15D[30m(B[m[38;2;0;95;215mcat[38;2;85;85;85m test-input.txt[15D[30m(B[m[38;2;0;95;215m test-input.txt[30m(B[m[15D [38;2;0;175;255m[4mtest-input.txt[30m(B[m
+[30m(B[m[?2004l]0;cat test-input.txt /home/thomas/Projects/zellij[30m(B[m
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+Velit ut tortor pretium viverra suspendisse potenti nullam ac tortor. Adipiscing elit ut aliquam purus sit amet luctus venenatis.
+Duis ut diam quam nulla porttitor massa id neque aliquam. Suspendisse potenti nullam ac tortor vitae purus faucibus ornare suspendisse.
+Vitae nunc sed velit dignissim sodales ut eu sem integer.
+Tortor id aliquet lectus proin nibh nisl.
+Commodo odio aenean sed adipiscing diam donec adipiscing tristique risus.
+Velit dignissim sodales ut eu sem. Lacus suspendisse faucibus interdum posuere lorem. Ac placerat vestibulum lectus mauris ultrices eros. Elementum integer enim neque volutpat ac. Augue interdum velit euismod in.
+
+Egestas sed sed risus pretium quam vulputate dignissim.
+Gravida rutrum quisque non tellus orci ac auctor augue.
+Risus nec feugiat in fermentum posuere urna nec tincidunt praesent.
+Elementum eu facilisis sed odio morbi quis.
+Mattis ullamcorper velit sed ullamcorper morbi.
+Dui vivamus arcu felis bibendum. Sit amet aliquam id diam.
+Suscipit tellus mauris a diam maecenas sed enim.
+Odio ut sem nulla pharetra.
+Cras ornare arcu dui vivamus arcu felis bibendum.
+Egestas fringilla phasellus faucibus scelerisque eleifend.
+Purus semper eget duis at tellus at urna condimentum.
+Aliquam etiam erat velit scelerisque in dictum non.
+Porta non pulvinar neque laoreet suspendisse interdum consectetur.
+Tempor nec feugiat nisl pretium. Sit amet consectetur adipiscing elit.
+Cras semper auctor neque vitae tempus quam pellentesque.
+Laoreet non curabitur gravida arcu ac tortor dignissim.
+Sed nisi lacus sed viverra tellus in.
+Rutrum tellus pellentesque eu tincidunt tortor aliquam nulla.
+
+Nascetur ridiculus mus mauris vitae ultricies leo integer malesuada.
+Interdum posuere lorem ipsum dolor sit amet consectetur.
+Porta non pulvinar neque laoreet suspendisse interdum.
+Fames ac turpis egestas integer eget aliquet nibh praesent.
+Congue nisi vitae suscipit tellus mauris a diam maecenas sed.
+Nec ultrices dui sapien eget mi proin sed libero enim.
+Tellus rutrum tellus pellentesque eu tincidunt.
+Ultrices eros in cursus turpis massa tincidunt dui ut ornare.
+Arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque.
+Viverra mauris in aliquam sem fringilla ut.
+Vulputate eu scelerisque felis imperdiet proin fermentum leo.
+Cursus risus at ultrices mi tempus.
+Laoreet id donec ultrices tincidunt arcu non sodales.
+Amet dictum sit amet justo donec enim.
+Hac habitasse platea dictumst vestibulum rhoncus est pellentesque.
+Facilisi cras fermentum odio eu feugiat.
+Elit ut aliquam purus sit amet luctus venena |