From f4ad946497264dbe8339f50b2e9ef0cf90a2024c Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 17 Aug 2022 09:28:51 +0200 Subject: fix(terminal): SGR/UTF8 mouse reporting in terminal panes (#1664) * work * work * fix: selection mishandling * style(fmt): rustfmt * style(comments): remove outdated * style(clippy): make clippy happy * fix(mouse): off by one sgr/utf8 reporting * style(fmt): rustfmt * fix(mouse): correctly report drag event code * fix(input): support mouse middle click * style(fmt): rustfmt --- zellij-client/src/input_handler.rs | 68 ++- zellij-server/src/panes/grid.rs | 510 ++++++++++++++++----- zellij-server/src/panes/plugin_pane.rs | 3 - zellij-server/src/panes/terminal_pane.rs | 26 +- zellij-server/src/pty_writer.rs | 2 +- zellij-server/src/route.rs | 41 +- zellij-server/src/screen.rs | 54 ++- zellij-server/src/tab/mod.rs | 256 ++++++++--- .../src/tab/unit/tab_integration_tests.rs | 336 +++++++++++++- zellij-server/src/thread_bus.rs | 8 + zellij-utils/src/errors.rs | 9 +- zellij-utils/src/input/actions.rs | 9 +- zellij-utils/src/position.rs | 7 + 13 files changed, 1118 insertions(+), 211 deletions(-) diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 7428bac0f..bebf1ee77 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -19,6 +19,19 @@ use zellij_utils::{ termwiz::input::InputEvent, }; +#[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 { @@ -31,7 +44,7 @@ struct InputHandler { send_client_instructions: SenderWithContext, should_exit: bool, receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, - holding_mouse: bool, + holding_mouse: Option, } impl InputHandler { @@ -54,7 +67,7 @@ impl InputHandler { send_client_instructions, should_exit: false, receive_input_instructions, - holding_mouse: false, + holding_mouse: None, } } @@ -161,30 +174,59 @@ impl InputHandler { self.dispatch_action(Action::ScrollDownAt(point), None); }, MouseButton::Left => { - if self.holding_mouse { - self.dispatch_action(Action::MouseHold(point), None); + if self.holding_mouse.is_some() { + self.dispatch_action(Action::MouseHoldLeft(point), None); } else { self.dispatch_action(Action::LeftClick(point), None); } - self.holding_mouse = true; + self.holding_mouse = Some(HeldMouseButton::Left); }, MouseButton::Right => { - if self.holding_mouse { - self.dispatch_action(Action::MouseHold(point), None); + if self.holding_mouse.is_some() { + self.dispatch_action(Action::MouseHoldRight(point), None); } else { self.dispatch_action(Action::RightClick(point), None); } - self.holding_mouse = true; + 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) => { - self.dispatch_action(Action::MouseRelease(point), None); - self.holding_mouse = false; + 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) => { - self.dispatch_action(Action::MouseHold(point), None); - self.holding_mouse = true; + 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); }, } } diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index fc815afdb..7150fd7a5 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -299,6 +299,28 @@ macro_rules! dump_screen { }}; } +fn utf8_mouse_coordinates(column: usize, line: isize) -> Vec { + let mut coordinates = vec![]; + let mouse_pos_encode = |pos: usize| -> Vec { + let pos = 32 + pos; + let first = 0xC0 + pos / 64; + let second = 0x80 + (pos & 63); + vec![first as u8, second as u8] + }; + + if column > 95 { + coordinates.append(&mut mouse_pos_encode(column)); + } else { + coordinates.push(32 + column as u8); + } + if line > 95 { + coordinates.append(&mut mouse_pos_encode(line as usize)); + } else { + coordinates.push(32 + line as u8); + } + coordinates +} + #[derive(Clone)] pub struct Grid { pub(crate) lines_above: VecDeque, @@ -340,11 +362,38 @@ pub struct Grid { pub link_handler: Rc>, pub ring_bell: bool, scrollback_buffer_lines: usize, - pub mouse_mode: bool, + pub mouse_mode: MouseMode, + pub mouse_tracking: MouseTracking, pub search_results: SearchResult, pub pending_clipboard_update: Option, } +#[derive(Clone, Debug)] +pub enum MouseMode { + NoEncoding, + Utf8, + Sgr, +} + +impl Default for MouseMode { + fn default() -> Self { + MouseMode::NoEncoding + } +} + +#[derive(Clone, Debug)] +pub enum MouseTracking { + Off, + Normal, + ButtonEventTracking, +} + +impl Default for MouseTracking { + fn default() -> Self { + MouseTracking::Off + } +} + impl Debug for Grid { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut buffer: Vec = self.viewport.clone(); @@ -444,7 +493,8 @@ impl Grid { link_handler, ring_bell: false, scrollback_buffer_lines: 0, - mouse_mode: false, + mouse_mode: MouseMode::default(), + mouse_tracking: MouseTracking::default(), character_cell_size, search_results: Default::default(), sixel_grid, @@ -1471,6 +1521,8 @@ impl Grid { self.scrollback_buffer_lines = 0; self.search_results = Default::default(); self.sixel_scrolling = false; + self.mouse_mode = MouseMode::NoEncoding; + self.mouse_tracking = MouseTracking::Off; if let Some(images_to_reap) = self.sixel_grid.clear() { self.sixel_grid.reap_images(images_to_reap); } @@ -1673,6 +1725,209 @@ impl Grid { } } } + 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' ' }; + let mut msg: Vec = vec![27, b'[', b'M', button_code]; + msg.append(&mut utf8_mouse_coordinates( + position.column() + 1, + position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }; + let sgr_event = || -> Option { + let button_code = if is_held { 32 } else { 0 }; + Some(format!( + "\u{1b}[<{:?};{:?};{:?}M", + button_code, + position.column() + 1, + position.line() + 1 + )) + }; + match (&self.mouse_mode, &self.mouse_tracking) { + (_, MouseTracking::Off) => None, + (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::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), + _ => None, + } + } + pub fn mouse_left_click_release_signal(&self, position: &Position) -> Option { + match (&self.mouse_mode, &self.mouse_tracking) { + (_, MouseTracking::Off) => None, + (MouseMode::NoEncoding | MouseMode::Utf8, _) => { + let mut msg: Vec = vec![27, b'[', b'M', b'#']; + msg.append(&mut utf8_mouse_coordinates( + position.column() + 1, + position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }, + (MouseMode::Sgr, _) => { + let mouse_event = format!( + "\u{1b}[<0;{:?};{:?}m", + position.column() + 1, + position.line() + 1 + ); + Some(mouse_event) + }, + } + } + pub fn mouse_right_click_signal(&self, position: &Position, is_held: bool) -> Option { + let utf8_event = || -> Option { + let button_code = if is_held { b'B' } else { b'"' }; + let mut msg: Vec = vec![27, b'[', b'M', button_code]; + msg.append(&mut utf8_mouse_coordinates( + position.column() + 1, + position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }; + let sgr_event = || -> Option { + let button_code = if is_held { 34 } else { 2 }; + Some(format!( + "\u{1b}[<{:?};{:?};{:?}M", + button_code, + position.column() + 1, + position.line() + 1 + )) + }; + match (&self.mouse_mode, &self.mouse_tracking) { + (_, MouseTracking::Off) => None, + (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::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), + _ => None, + } + } + pub fn mouse_right_click_release_signal(&self, position: &Position) -> Option { + match (&self.mouse_mode, &self.mouse_tracking) { + (_, MouseTracking::Off) => None, + (MouseMode::NoEncoding | MouseMode::Utf8, _) => { + let mut msg: Vec = vec![27, b'[', b'M', b'#']; + msg.append(&mut utf8_mouse_coordinates( + position.column() + 1, + position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }, + (MouseMode::Sgr, _) => { + let mouse_event = format!( + "\u{1b}[<2;{:?};{:?}m", + position.column() + 1, + position.line() + 1 + ); + Some(mouse_event) + }, + } + } + pub fn mouse_middle_click_signal(&self, position: &Position, is_held: bool) -> Option { + let utf8_event = || -> Option { + let button_code = if is_held { b'A' } else { b'!' }; + let mut msg: Vec = vec![27, b'[', b'M', button_code]; + msg.append(&mut utf8_mouse_coordinates( + position.column() + 1, + position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }; + let sgr_event = || -> Option { + let button_code = if is_held { 33 } else { 1 }; + Some(format!( + "\u{1b}[<{:?};{:?};{:?}M", + button_code, + position.column() + 1, + position.line() + 1 + )) + }; + match (&self.mouse_mode, &self.mouse_tracking) { + (_, MouseTracking::Off) => None, + (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::Sgr, MouseTracking::Normal) if !is_held => sgr_event(), + _ => None, + } + } + pub fn mouse_middle_click_release_signal(&self, position: &Position) -> Option { + match (&self.mouse_mode, &self.mouse_tracking) { + (_, MouseTracking::Off) => None, + (MouseMode::NoEncoding | MouseMode::Utf8, _) => { + let mut msg: Vec = vec![27, b'[', b'M', b'#']; + msg.append(&mut utf8_mouse_coordinates( + position.column() + 1, + position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }, + (MouseMode::Sgr, _) => { + // TODO: these don't add a +1 because it's done outside, we should change it to + // happen here for consistency + let mouse_event = format!( + "\u{1b}[<1;{:?};{:?}m", + position.column() + 1, + position.line() + 1 + ); + Some(mouse_event) + }, + } + } + pub fn mouse_scroll_up_signal(&self, position: &Position) -> Option { + match (&self.mouse_mode, &self.mouse_tracking) { + (_, MouseTracking::Off) => None, + (MouseMode::NoEncoding | MouseMode::Utf8, _) => { + let mut msg: Vec = vec![27, b'[', b'M', b'`']; + msg.append(&mut utf8_mouse_coordinates( + position.column() + 1, + position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }, + (MouseMode::Sgr, _) => { + let mouse_event = format!( + "\u{1b}[<64;{:?};{:?}M", + position.column.0 + 1, + position.line.0 + 1 + ); + Some(mouse_event) + }, + } + } + pub fn mouse_scroll_down_signal(&self, position: &Position) -> Option { + match (&self.mouse_mode, &self.mouse_tracking) { + (_, MouseTracking::Off) => None, + (MouseMode::NoEncoding | MouseMode::Utf8, _) => { + let mut msg: Vec = vec![27, b'[', b'M', b'a']; + msg.append(&mut utf8_mouse_coordinates( + position.column() + 1, + position.line() + 1, + )); + Some(String::from_utf8_lossy(&msg).into()) + }, + (MouseMode::Sgr, _) => { + let mouse_event = format!( + "\u{1b}[<65;{:?};{:?}M", + position.column.0 + 1, + position.line.0 + 1 + ); + Some(mouse_event) + }, + } + } } impl Perform for Grid { @@ -2027,59 +2282,74 @@ impl Perform for Grid { _ => false, }; if first_intermediate_is_questionmark { - match params_iter.next().map(|param| param[0]) { - Some(2004) => { - self.bracketed_paste_mode = false; - }, - Some(1049) => { - if let Some(mut alternate_screen_state) = self.alternate_screen_state.take() - { - if let Some(image_ids_to_reap) = self.sixel_grid.clear() { - // reap images before dropping the alternate_screen_state contents - // - we can't implement a drop method for this because the store is - // outside of the alternate_screen_state struct - self.sixel_grid.reap_images(image_ids_to_reap); + for param in params_iter.map(|param| param[0]) { + match param { + 2004 => { + self.bracketed_paste_mode = false; + }, + 1049 => { + if let Some(mut alternate_screen_state) = + self.alternate_screen_state.take() + { + if let Some(image_ids_to_reap) = self.sixel_grid.clear() { + // reap images before dropping the alternate_screen_state contents + // - we can't implement a drop method for this because the store is + // outside of the alternate_screen_state struct + self.sixel_grid.reap_images(image_ids_to_reap); + } + alternate_screen_state.apply_contents_to( + &mut self.lines_above, + &mut self.viewport, + &mut self.cursor, + &mut self.sixel_grid, + ); } - alternate_screen_state.apply_contents_to( - &mut self.lines_above, - &mut self.viewport, - &mut self.cursor, - &mut self.sixel_grid, - ); - } - self.alternate_screen_state = None; - self.clear_viewport_before_rendering = true; - self.force_change_size(self.height, self.width); // the alternative_viewport might have been of a different size... - self.mark_for_rerender(); - }, - Some(25) => { - self.hide_cursor(); - self.mark_for_rerender(); - }, - Some(1) => { - self.cursor_key_mode = false; - }, - Some(3) => { - // DECCOLM - only side effects - self.scroll_region = None; - self.clear_all(EMPTY_TERMINAL_CHARACTER); - self.cursor.x = 0; - self.cursor.y = 0; - }, - Some(6) => { - self.erasure_mode = false; - }, - Some(7) => { - self.disable_linewrap = true; - }, - Some(80) => { - self.sixel_scrolling = false; - }, - Some(1006) => { - self.mouse_mode = false; - }, - _ => {}, - }; + self.alternate_screen_state = None; + self.clear_viewport_before_rendering = true; + self.force_change_size(self.height, self.width); // the alternative_viewport might have been of a different size... + self.mark_for_rerender(); + }, + 25 => { + self.hide_cursor(); + self.mark_for_rerender(); + }, + 1 => { + self.cursor_key_mode = false; + }, + 3 => { + // DECCOLM - only side effects + self.scroll_region = None; + self.clear_all(EMPTY_TERMINAL_CHARACTER); + self.cursor.x = 0; + self.cursor.y = 0; + }, + 6 => { + self.erasure_mode = false; + }, + 7 => { + self.disable_linewrap = true; + }, + 80 => { + self.sixel_scrolling = false; + }, + 1000 => { + self.mouse_tracking = MouseTracking::Off; + }, + 1002 => { + self.mouse_tracking = MouseTracking::Off; + }, + 1003 => { + // TBD: any-even mouse tracking + }, + 1005 => { + self.mouse_mode = MouseMode::NoEncoding; + }, + 1006 => { + self.mouse_mode = MouseMode::NoEncoding; + }, + _ => {}, + }; + } } else if let Some(4) = params_iter.next().map(|param| param[0]) { self.insert_mode = false; } @@ -2090,64 +2360,80 @@ impl Perform for Grid { _ => false, }; if first_intermediate_is_questionmark { - match params_iter.next().map(|param| param[0]) { - Some(25) => { - self.show_cursor(); - self.mark_for_rerender(); - }, - Some(2004) => { - self.bracketed_paste_mode = true; - }, - Some(1049) => { - // enter alternate buffer - let current_lines_above = std::mem::replace( - &mut self.lines_above, - VecDeque::with_capacity(*SCROLL_BUFFER_SIZE.get().unwrap()), - ); - let current_viewport = std::mem::replace( - &mut self.viewport, - vec![Row::new(self.width).canonical()], - ); - let current_cursor = std::mem::replace(&mut self.cursor, Cursor::new(0, 0)); - let sixel_image_store = self.sixel_grid.sixel_image_store.clone(); - let alternate_sixelgrid = std::mem::replace( - &mut self.sixel_grid, - SixelGrid::new(self.character_cell_size.clone(), sixel_image_store), - ); - self.alternate_screen_state = Some(AlternateScreenState::new( - current_lines_above, - current_viewport, - current_cursor, - alternate_sixelgrid, - )); - self.clear_viewport_before_rendering = true; - self.scrollback_buffer_lines = self.recalculate_scrollback_buffer_count(); - self.output_buffer.update_all_lines(); // make sure the screen gets cleared in the next render - }, - Some(1) => { - self.cursor_key_mode = true; - }, - Some(3) => { - // DECCOLM - only side effects - self.scroll_region = None; - self.clear_all(EMPTY_TERMINAL_CHARACTER); - self.cursor.x = 0; - self.cursor.y = 0; - }, - Some(6) => { - self.erasure_mode = true; - }, - Some(7) => { - self.disable_linewrap = false; - }, - Some(80) => { - self.sixel_scrolling = true; - }, - Some(1006) => { - self.mouse_mode = true; - }, - _ => {}, - }; + for param in params_iter.map(|param| param[0]) { + match param { + 25 => { + self.show_cursor(); + self.mark_for_rerender(); + }, + 2004 => { + self.bracketed_paste_mode = true; + }, + 1049 => { + // enter alternate buffer + let current_lines_above = std::mem::replace( + &mut self.lines_above, + VecDeque::with_capacity(*SCROLL_BUFFER_SIZE.get().unwrap()), + ); + let current_viewport = std::mem::replace( + &mut self.viewport, + vec![Row::new(self.width).canonical()], + ); + let current_cursor = + std::mem::replace(&mut self.cursor, Cursor::new(0, 0)); + let sixel_image_store = self.sixel_grid.sixel_image_store.clone(); + let alternate_sixelgrid = std::mem::replace( + &mut self.sixel_grid, + SixelGrid::new(self.character_cell_size.clone(), sixel_image_store), + ); + self.alternate_screen_state = Some(AlternateScreenState::new( + current_lines_above, + current_viewport, + current_cursor, + alternate_sixelgrid, + )); + self.clear_viewport_before_rendering = true; + self.scrollback_buffer_lines = + self.recalculate_scrollback_buffer_count(); + self.output_buffer.update_all_lines(); // make sure the screen gets cleared in the next render + }, + 1 => { + self.cursor_key_mode = true; + }, + 3 => { + // DECCOLM - only side effects + self.scroll_region = None; + self.clear_all(EMPTY_TERMINAL_CHARACTER); + self.cursor.x = 0; + self.cursor.y = 0; + }, + 6 => { + self.erasure_mode = true; + }, + 7 => { + self.disable_linewrap = false; + }, + 80 => { + self.sixel_scrolling = true; + }, + 1000 => { + self.mouse_tracking = MouseTracking::Normal; + }, + 1002 => { + self.mouse_tracking = MouseTracking::ButtonEventTracking; + }, + 1003 => { + // TBD: any-even mouse tracking + }, + 1005 => { + self.mouse_mode = MouseMode::Utf8; + }, + 1006 => { + self.mouse_mode = MouseMode::Sgr; + }, + _ => {}, + } + } } else if let Some(4) = params_iter.next().map(|param| param[0]) { self.insert_mode = true; } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 7d56b589d..2ee865fe0 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -415,7 +415,4 @@ impl Pane for PluginPane { )) .unwrap(); } - fn mouse_mode(&self) -> bool { - false - } } diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index eb884f536..fcb1f63cd 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -566,10 +566,30 @@ impl Pane for TerminalPane { self.borderless } - fn mouse_mode(&self) -> bool { - self.grid.mouse_mode + fn mouse_left_click(&self, position: &Position, is_held: bool) -> Option { + self.grid.mouse_left_click_signal(position, is_held) + } + fn mouse_left_click_release(&self, position: &Position) -> Option { + self.grid.mouse_left_click_release_signal(position) + } + fn mouse_right_click(&self, position: &Position, is_held: bool) -> Option { + self.grid.mouse_right_click_signal(position, is_held) + } + fn mouse_right_click_release(&self, position: &Position) -> Option { + self.grid.mouse_right_click_release_signal(position) + } + fn mouse_middle_click(&self, position: &Position, is_held: bool) -> Option { + self.grid.mouse_middle_click_signal(position, is_held) + } + fn mouse_middle_click_release(&self, position: &Position) -> Option { + self.grid.mouse_middle_click_release_signal(position) + } + fn mouse_scroll_up(&self, position: &Position) -> Option { + self.grid.mouse_scroll_up_signal(position) + } + fn mouse_scroll_down(&self, position: &Position) -> Option { + self.grid.mouse_scroll_down_signal(position) } - fn get_line_number(&self) -> Option { // + 1 because the absolute position in the scrollback is 0 indexed and this should be 1 indexed Some(self.grid.absolute_position_in_scrollback() + 1) diff --git a/zellij-server/src/pty_writer.rs b/zellij-server/src/pty_writer.rs index f0aeaf42b..089dfc75e 100644 --- a/zellij-server/src/pty_writer.rs +++ b/zellij-server/src/pty_writer.rs @@ -2,7 +2,7 @@ use zellij_utils::errors::{ContextType, PtyWriteContext}; use crate::thread_bus::Bus; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub(crate) enum PtyWriteInstruction { Write(Vec, i32), Exit, diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 81b1fe047..92b8f9c91 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -33,7 +33,7 @@ fn route_action( // 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::MouseHold(_) => {}, + Action::MouseHoldLeft(..) | Action::MouseHoldRight(..) => {}, _ => { session .senders @@ -376,17 +376,46 @@ fn route_action( .send_to_screen(ScreenInstruction::RightClick(point, client_id)) .unwrap(); }, - - Action::MouseRelease(point) => { + Action::MiddleClick(point) => { + session + .senders + .send_to_screen(ScreenInstruction::MiddleClick(point, client_id)) + .unwrap(); + }, + Action::LeftMouseRelease(point) => { + session + .senders + .send_to_screen(ScreenInstruction::LeftMouseRelease(point, client_id)) + .unwrap(); + }, + Action::RightMouseRelease(point) => { + session + .senders + .send_to_screen(ScreenInstruction::RightMouseRelease(point, client_id)) + .unwrap(); + }, + Action::MiddleMouseRelease(point) => { + session + .senders + .send_to_screen(ScreenInstruction::MiddleMouseRelease(point, client_id)) + .unwrap(); + }, + Action::MouseHoldLeft(point) => { + session + .senders + .send_to_screen(ScreenInstruction::MouseHoldLeft(point, client_id)) + .unwrap(); + }, + Action::MouseHoldRight(point) => { session .senders - .send_to_screen(ScreenInstruction::MouseRelease(point, client_id)) + .send_to_screen(ScreenInstruction::MouseHoldRight(point, client_id)) .unwrap(); }, - Action::MouseHold(point) => { + Action::MouseHoldMiddle(point) => { session .senders - .send_to_screen(ScreenInstruction::MouseHold(point, client_id)) + .send_to_screen(ScreenInstruction::MouseHoldMiddle(point, client_id)) .unwrap(); }, Action::Copy => { diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index c184e29fb..deba6eb9c 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -123,8 +123,13 @@ pub enum ScreenInstruction { ChangeMode(ModeInfo, ClientId), LeftClick(Position, ClientId), RightClick(Position, ClientId), - MouseRelease(Position, ClientId), - MouseHold(Position, ClientId), + MiddleClick(Position, ClientId), + LeftMouseRelease(Position, ClientId), + RightMouseRelease(Position, ClientId), + MiddleMouseRelease(Position, ClientId), + MouseHoldLeft(Position, ClientId), + MouseHoldRight(Position, ClientId), + MouseHoldMiddle(Position, ClientId), Copy(ClientId), AddClient(ClientId), RemoveClient(ClientId), @@ -222,8 +227,13 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ScrollDownAt(..) => ScreenContext::ScrollDownAt, ScreenInstruction::LeftClick(..) => ScreenContext::LeftClick, ScreenInstruction::RightClick(..) => ScreenContext::RightClick, - ScreenInstruction::MouseRelease(..) => ScreenContext::MouseRelease, - ScreenInstruction::MouseHold(..) => ScreenContext::MouseHold, + 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::Copy(..) => ScreenContext::Copy, ScreenInstruction::ToggleTab(..) => ScreenContext::ToggleTab, ScreenInstruction::AddClient(..) => ScreenContext::AddClient, @@ -1303,14 +1313,42 @@ pub(crate) fn screen_thread_main( screen.update_tabs(); screen.render(); }, - ScreenInstruction::MouseRelease(point, client_id) => { + ScreenInstruction::MiddleClick(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab - .handle_mouse_release(&point, client_id)); + .handle_middle_click(&point, client_id)); + screen.update_tabs(); + screen.render(); + }, + ScreenInstruction::LeftMouseRelease(point, client_id) => { + active_tab!(screen, client_id, |tab: &mut Tab| tab + .handle_left_mouse_release(&point, client_id)); + screen.render(); + }, + ScreenInstruction::RightMouseRelease(point, client_id) => { + active_tab!(screen, client_id, |tab: &mut Tab| tab + .handle_right_mouse_release(&point, client_id)); + screen.render(); + }, + ScreenInstruction::MiddleMouseRelease(point, client_id) => { + active_tab!(screen, client_id, |tab: &mut Tab| tab + .handle_middle_mouse_release(&point, client_id)); + screen.render(); + }, + ScreenInstruction::MouseHoldLeft(point, client_id) => { + active_tab!(screen, client_id, |tab: &mut Tab| { + tab.handle_mouse_hold_left(&point, client_id); + }); + screen.render(); + }, + ScreenInstruction::MouseHoldRight(point, client_id) => { + active_tab!(screen, client_id, |tab: &mut Tab| { + tab.handle_mouse_hold_right(&point, client_id); + }); screen.render(); }, - ScreenInstruction::MouseHold(point, client_id) => { + ScreenInstruction::MouseHoldMiddle(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| { - tab.handle_mouse_hold(&point, client_id); + tab.handle_mouse_hold_middle(&point, client_id); }); screen.render(); }, diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 44c2a480f..750f49447 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -296,8 +296,32 @@ pub trait Pane { fn load_pane_name(&mut self); fn set_borderless(&mut self, borderless: bool); fn borderless(&self) -> bool; + // TODO: this should probably be merged with the mouse_right_click fn handle_right_click(&mut self, _to: &Position, _client_id: ClientId) {} - fn mouse_mode(&self) -> bool; + fn mouse_left_click(&self, _position: &Position, _is_held: bool) -> Option { + None + } + fn mouse_left_click_release(&self, _position: &Position) -> Option { + None + } + fn mouse_right_click(&self, _position: &Position, _is_held: bool) -> Option { + None + } + fn mouse_right_click_release(&self, _position: &Position) -> Option { + None + } + fn mouse_middle_click(&self, _position: &Position, _is_held: bool) -> Option { + None + } + fn mouse_middle_click_release(&self, _position: &Position) -> Option { + None + } + fn mouse_scroll_up(&self, _position: &Position) -> Option { + None + } + fn mouse_scroll_down(&self, _position: &Position) -> Option { + None + } fn get_line_number(&self) -> Option { None } @@ -1668,13 +1692,8 @@ impl Tab { } pub fn scroll_terminal_up(&mut self, point: &Position, lines: usize, client_id: ClientId) { if let Some(pane) = self.get_pane_at(point, false) { - if pane.mouse_mode() { - let relative_position = pane.relative_position(point); - let mouse_event = format!( - "\u{1b}[<64;{:?};{:?}M", - relative_position.column.0 + 1, - relative_position.line.0 + 1 - ); + let relative_position = pane.relative_position(point); + if let Some(mouse_event) = pane.mouse_scroll_up(&relative_position) { self.write_to_terminal_at(mouse_event.into_bytes(), point); } else { pane.scroll_up(lines, client_id); @@ -1683,13 +1702,8 @@ impl Tab { } pub fn scroll_terminal_down(&mut self, point: &Position, lines: usize, client_id: ClientId) { if let Some(pane) = self.get_pane_at(point, false) { - if pane.mouse_mode() { - let relative_position = pane.relative_position(point); - let mouse_event = format!( - "\u{1b}[<65;{:?};{:?}M", - relative_position.column.0 + 1, - relative_position.line.0 + 1 - ); + let relative_position = pane.relative_position(point); + if let Some(mouse_event) = pane.mouse_scroll_down(&relative_position) { self.write_to_terminal_at(mouse_event.into_bytes(), point); } else { pane.scroll_down(lines, client_id); @@ -1755,18 +1769,11 @@ impl Tab { if let Some(pane) = self.get_pane_at(position, false) { let relative_position = pane.relative_position(position); - - if pane.mouse_mode() { + if let Some(mouse_event) = pane.mouse_left_click(&relative_position, false) { if !pane.position_is_on_frame(position) { - let mouse_event = format!( - "\u{1b}[<0;{:?};{:?}M", - relative_position.column() + 1, - relative_position.line() + 1 - ); self.write_to_active_terminal(mouse_event.into_bytes(), client_id); } } else { - // TODO: rename this method, it is used to forward click events to plugin panes pane.start_selection(&relative_position, client_id); if let PaneId::Terminal(_) = pane.pid() { self.selecting_with_mouse = true; @@ -1779,13 +1786,8 @@ impl Tab { if let Some(pane) = self.get_pane_at(position, false) { let relative_position = pane.relative_position(position); - if pane.mouse_mode() { + if let Some(mouse_event) = pane.mouse_right_click(&relative_position, false) { if !pane.position_is_on_frame(position) { - let mouse_event = format!( - "\u{1b}[<2;{:?};{:?}M", - relative_position.column() + 1, - relative_position.line() + 1 - ); self.write_to_active_terminal(mouse_event.into_bytes(), client_id); } } else { @@ -1793,6 +1795,18 @@ impl Tab { } }; } + pub fn handle_middle_click(&mut self, position: &Position, client_id: ClientId) { + self.focus_pane_at(position, client_id); + + if let Some(pane) = self.get_pane_at(position, false) { + 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(mouse_event.into_bytes(), client_id); + } + } + }; + } fn focus_pane_at(&mut self, point: &Position, client_id: ClientId) { if self.floating_panes.panes_are_visible() { if let Some(clicked_pane) = self.floating_panes.get_pane_id_at(point, true) { @@ -1810,7 +1824,51 @@ impl Tab { } } } - pub fn handle_mouse_release(&mut self, position: &Position, client_id: ClientId) { + pub fn handle_right_mouse_release(&mut self, position: &Position, client_id: ClientId) { + self.last_mouse_hold_position = None; + 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_right_click_release(&relative_position) { + self.write_to_active_terminal(mouse_event.into_bytes(), client_id); + } + } + } + pub fn handle_middle_mouse_release(&mut self, position: &Position, client_id: ClientId) { + self.last_mouse_hold_position = None; + 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_middle_click_release(&relative_position) { + self.write_to_active_terminal(mouse_event.into_bytes(), client_id); + } + } + } + pub fn handle_left_mouse_release(&mut self, position: &Position, client_id: ClientId) { self.last_mouse_hold_position = None; if self.floating_panes.panes_are_visible() @@ -1826,20 +1884,23 @@ impl Tab { 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(position); - if active_pane.mouse_mode() { - // ensure that coordinates are valid - let col = (relative_position.column() + 1) - .max(1) - .min(active_pane.get_content_columns()); - - let line = (relative_position.line() + 1) - .max(1) - .min(active_pane.get_content_rows() as isize); - let mouse_event = format!("\u{1b}[<0;{:?};{:?}m", col, line); + 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(mouse_event.into_bytes(), client_id); } else { - // TODO: rename this method, it is used to forward release events to plugin panes + let relative_position = active_pane.relative_position(position); if let PaneId::Terminal(_) = active_pane.pid() { if selecting && copy_on_release { active_pane.end_selection(&relative_position, client_id); @@ -1858,7 +1919,7 @@ impl Tab { } } } - pub fn handle_mouse_hold( + pub fn handle_mouse_hold_left( &mut self, position_on_screen: &Position, client_id: ClientId, @@ -1889,20 +1950,24 @@ impl Tab { 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(position_on_screen); - if active_pane.mouse_mode() && !is_repeated { + let mut relative_position = active_pane.relative_position(position_on_screen); + if !is_repeated { // ensure that coordinates are valid - let col = (relative_position.column() + 1) - .max(1) - .min(active_pane.get_content_columns()); - - let line = (relative_position.line() + 1) - .max(1) - .min(active_pane.get_content_rows() as isize); + relative_position.change_column( + (relative_position.column()) + .max(0) + .min(active_pane.get_content_columns()), + ); - let mouse_event = format!("\u{1b}[<32;{:?};{:?}M", col, line); - self.write_to_active_terminal(mouse_event.into_bytes(), client_id); - return true; // we need to re-render in this case so the selection disappears + 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(mouse_event.into_bytes(), client_id); + return 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 true; // we need to re-render in this case so the selection is updated @@ -1910,6 +1975,87 @@ impl Tab { } 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, + ) -> bool { + // 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(mouse_event.into_bytes(), client_id); + return true; // we need to re-render in this case so the selection disappears + } + } + } + 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, + ) -> bool { + println!("mouse hold middle"); + // 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 + }; + println!("is repeated: {:?}", is_repeated); + 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 { + println!("can have 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) + { + log::info!("can have mouse event: {:?}", mouse_event); + self.write_to_active_terminal(mouse_event.into_bytes(), client_id); + return true; // we need to re-render in this case so the selection disappears + } + } + } + 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) { let selected_text = self diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index bb6dbf653..2496f89d0 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -17,6 +17,9 @@ use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::position::Position; +use crate::pty_writer::PtyWriteInstruction; +use zellij_utils::channels::{self, ChannelWithContext, SenderWithContext}; + use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::os::unix::io::RawFd; @@ -150,6 +153,63 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { tab } +fn create_new_tab_with_mock_pty_writer( + size: Size, + default_mode: ModeInfo, + mock_pty_writer: SenderWithContext, +) -> Tab { + set_session_name("test".into()); + let index = 0; + let position = 0; + let name = String::new(); + let os_api = Box::new(FakeInputOutput { + file_dumps: Arc::new(Mutex::new(HashMap::new())), + }); + let mut senders = ThreadSenders::default().silently_fail_on_send(); + senders.replace_to_pty_writer(mock_pty_writer); + let max_panes = None; + let mode_info = default_mode; + let style = Style::default(); + let draw_pane_frames = true; + let client_id = 1; + let session_is_mirrored = true; + let mut connected_clients = HashSet::new(); + connected_clients.insert(client_id); + let connected_clients = Rc::new(RefCell::new(connected_clients)); + let character_cell_info = Rc::new(RefCell::new(None)); + let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); + let copy_options = CopyOptions::default(); + let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); + let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); + let mut tab = Tab::new( + index, + position, + name, + size, + character_cell_info, + sixel_image_store, + os_api, + senders, + max_panes, + style, + mode_info, + draw_pane_frames, + connected_clients, + session_is_mirrored, + client_id, + copy_options, + terminal_emulator_colors, + terminal_emulator_color_codes, + ); + tab.apply_layout( + LayoutTemplate::default().try_into().unwrap(), + vec![1], + index, + client_id, + ); + tab +} + fn create_new_tab_with_sixel_support( size: Size, sixel_image_store: Rc>, @@ -846,7 +906,7 @@ fn move_floating_pane_focus_with_mouse() { tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); tab.handle_left_click(&Position::new(9, 71), client_id); - tab.handle_mouse_release(&Position::new(9, 71), client_id); + tab.handle_left_mouse_release(&Position::new(9, 71), client_id); tab.render(&mut output, None); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().get(&client_id).unwrap(), @@ -892,7 +952,7 @@ fn move_pane_focus_with_mouse_to_non_floating_pane() { tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); tab.handle_left_click(&Position::new(4, 71), client_id); - tab.handle_mouse_release(&Position::new(4, 71), client_id); + tab.handle_left_mouse_release(&Position::new(4, 71), client_id); tab.render(&mut output, None); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().get(&client_id).unwrap(), @@ -938,7 +998,7 @@ fn drag_pane_with_mouse() { tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); tab.handle_left_click(&Position::new(5, 71), client_id); - tab.handle_mouse_release(&Position::new(7, 75), client_id); + tab.handle_left_mouse_release(&Position::new(7, 75), client_id); tab.render(&mut output, None); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().get(&client_id).unwrap(), @@ -988,7 +1048,7 @@ fn mark_text_inside_floating_pane() { tab.selecting_with_mouse, "started selecting with mouse on click" ); - tab.handle_mouse_release(&Position::new(8, 50), client_id); + tab.handle_left_mouse_release(&Position::new(8, 50), client_id); assert!( !tab.selecting_with_mouse, "stopped selecting with mouse on release" @@ -1357,7 +1417,7 @@ fn move_floating_pane_with_sixel_image() { let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(2, fixture); tab.handle_left_click(&Position::new(5, 71), client_id); - tab.handle_mouse_release(&Position::new(7, 75), client_id); + tab.handle_left_mouse_release(&Position::new(7, 75), client_id); tab.render(&mut output, None); let snapshot = take_snapshot_with_sixel( @@ -1392,7 +1452,7 @@ fn floating_pane_above_sixel_image() { let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(1, fixture); tab.handle_left_click(&Position::new(5, 71), client_id); - tab.handle_mouse_release(&Position::new(7, 75), client_id); + tab.handle_left_mouse_release(&Position::new(7, 75), client_id); tab.render(&mut output, None); let snapshot = take_snapshot_with_sixel( @@ -1720,3 +1780,267 @@ fn enter_search_floating_pane() { ); assert_snapshot!("search_floating_tab_highlight_fring", snapshot); } + +#[test] +fn pane_in_sgr_button_event_tracking_mouse_mode() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + + let messages_to_pty_writer = Arc::new(Mutex::new(vec![])); + let (to_pty_writer, pty_writer_receiver): ChannelWithContext = + channels::unbounded(); + let to_pty_writer = SenderWithContext::new(to_pty_writer); + let mut tab = create_new_tab_with_mock_pty_writer(size, ModeInfo::default(), to_pty_writer); + + // TODO: note that this thread does not die when the test dies + // it only dies once all the test process exits... not a biggy if we have only a handful of + // these, but otherwise we might want to think of a better way to handle this + let _pty_writer_thread = std::thread::Builder::new() + .name("pty_writer".to_string()) + .spawn({ + // TODO: kill this thread + let messages_to_pty_writer = messages_to_pty_writer.clone(); + move || loop { + let (event, _err_ctx) = pty_writer_receiver + .recv() + .expect("failed to receive event on channel"); + if let PtyWriteInstruction::Write(msg, _) = event { + messages_to_pty_writer + .lock() + .unwrap() + .push(String::from_utf8_lossy(&msg).to_string()); + } + } + }); + 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()); + tab.handle_left_click(&Position::new(5, 71), client_id); + tab.handle_mouse_hold_left(&Position::new(9, 72), client_id); + tab.handle_left_mouse_release(&Position::new(7, 75), client_id); + tab.handle_right_click(&Position::new(5, 71), client_id); + tab.handle_mouse_hold_right(&Position::new(9, 72), client_id); + tab.handle_right_mouse_release(&Position::new(7, 75), client_id); + tab.handle_middle_click(&Position::new(5, 71), client_id); + tab.handle_mouse_hold_middle(&Position::new(9, 72), client_id); + tab.handle_middle_mouse_release(&Position::new(7, 75), client_id); + tab.scroll_terminal_up(&Position::new(5, 71), 1, client_id); + tab.scroll_terminal_down(&Position::new(5, 71), 1, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); // give time for messages to arrive + assert_eq!( + *messages_to_pty_writer.lock().unwrap(), + 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 + ] + ); +} + +#[test] +fn pane_in_sgr_normal_event_tracking_mouse_mode() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + + let