summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2022-08-17 09:28:51 +0200
committerGitHub <noreply@github.com>2022-08-17 09:28:51 +0200
commitf4ad946497264dbe8339f50b2e9ef0cf90a2024c (patch)
tree1a53e6f56d1a8b491457266fadc1d93f118be6b2
parentb53e3807eb682ba395a7c4f31ace42d67dca5d88 (diff)
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
-rw-r--r--zellij-client/src/input_handler.rs68
-rw-r--r--zellij-server/src/panes/grid.rs510
-rw-r--r--zellij-server/src/panes/plugin_pane.rs3
-rw-r--r--zellij-server/src/panes/terminal_pane.rs26
-rw-r--r--zellij-server/src/pty_writer.rs2
-rw-r--r--zellij-server/src/route.rs41
-rw-r--r--zellij-server/src/screen.rs54
-rw-r--r--zellij-server/src/tab/mod.rs256
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs336
-rw-r--r--zellij-server/src/thread_bus.rs8
-rw-r--r--zellij-utils/src/errors.rs9
-rw-r--r--zellij-utils/src/input/actions.rs9
-rw-r--r--zellij-utils/src/position.rs7
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<ClientInstruction>,
should_exit: bool,
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
- holding_mouse: bool,
+ holding_mouse: Option<HeldMouseButton>,
}
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<u8> {
+ let mut coordinates = vec![];
+ let mouse_pos_encode = |pos: usize| -> Vec<u8> {
+ 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<Row>,
@@ -340,11 +362,38 @@ pub struct Grid {
pub link_handler: Rc<RefCell<LinkHandler>>,
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<String>,
}
+#[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<Row> = 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<String> {
+ let utf8_event = || -> Option<String> {
+ let button_code = if is_held { b'@' } else { b' ' };
+ let mut msg: Vec<u8> = 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<String> {
+ 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<String> {
+ match (&self.mouse_mode, &self.mouse_tracking) {
+ (_, MouseTracking::Off) => None,
+ (MouseMode::NoEncoding | MouseMode::Utf8, _) => {
+ let mut msg: Vec<u8> = 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<String> {
+ let utf8_event = || -> Option<String> {
+ let button_code = if is_held { b'B' } else { b'"' };
+ let mut msg: Vec<u8> = 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<String> {
+ 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<String> {
+ match (&self.mouse_mode, &self.mouse_tracking) {
+ (_, MouseTracking::Off) => None,
+ (MouseMode::NoEncoding | MouseMode::Utf8, _) => {
+ let mut msg: Vec<u8> = 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<String> {
+ let utf8_event = || -> Option<String> {
+ let button_code = if is_held { b'A' } else { b'!' };
+ let mut msg: Vec<u8> = 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<String> {
+ 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<String> {
+ match (&self.mouse_mode, &self.mouse_tracking) {
+ (_, MouseTracking::Off) => None,
+ (MouseMode::NoEncoding | MouseMode::Utf8, _) => {
+ let mut msg: Vec<u8> = 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<String> {
+ match (&self.mouse_mode, &self.mouse_tracking) {
+ (_, MouseTracking::Off) => None,
+ (MouseMode::NoEncoding | MouseMode::Utf8, _) => {
+ let mut msg: Vec<u8> = 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<String> {
+ match (&self.mouse_mode, &self.mouse_tracking) {
+ (_, MouseTracking::Off) => None,
+ (MouseMode::NoEncoding | MouseMode::Utf8, _) => {
+ let mut msg: Vec<u8> = 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;
- },
-