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
This commit is contained in:
Aram Drevekenin 2022-08-17 09:28:51 +02:00 committed by GitHub
parent b53e3807eb
commit f4ad946497
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1117 additions and 210 deletions

View file

@ -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);
},
}
}

View file

@ -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;
},
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;
}

View file

@ -415,7 +415,4 @@ impl Pane for PluginPane {
))
.unwrap();
}
fn mouse_mode(&self) -> bool {
false
}
}

View file

@ -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<String> {
self.grid.mouse_left_click_signal(position, is_held)
}
fn mouse_left_click_release(&self, position: &Position) -> Option<String> {
self.grid.mouse_left_click_release_signal(position)
}
fn mouse_right_click(&self, position: &Position, is_held: bool) -> Option<String> {
self.grid.mouse_right_click_signal(position, is_held)
}
fn mouse_right_click_release(&self, position: &Position) -> Option<String> {
self.grid.mouse_right_click_release_signal(position)
}
fn mouse_middle_click(&self, position: &Position, is_held: bool) -> Option<String> {
self.grid.mouse_middle_click_signal(position, is_held)
}
fn mouse_middle_click_release(&self, position: &Position) -> Option<String> {
self.grid.mouse_middle_click_release_signal(position)
}
fn mouse_scroll_up(&self, position: &Position) -> Option<String> {
self.grid.mouse_scroll_up_signal(position)
}
fn mouse_scroll_down(&self, position: &Position) -> Option<String> {
self.grid.mouse_scroll_down_signal(position)
}
fn get_line_number(&self) -> Option<usize> {
// + 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)

View file

@ -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<u8>, i32),
Exit,

View file

@ -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::MouseRelease(point, client_id))
.send_to_screen(ScreenInstruction::MiddleClick(point, client_id))
.unwrap();
},
Action::MouseHold(point) => {
Action::LeftMouseRelease(point) => {
session
.senders
.send_to_screen(ScreenInstruction::MouseHold(point, client_id))
.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::MouseHoldRight(point, client_id))
.unwrap();
},
Action::MouseHoldMiddle(point) => {
session
.senders
.send_to_screen(ScreenInstruction::MouseHoldMiddle(point, client_id))
.unwrap();
},
Action::Copy => {

View file

@ -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::MouseHold(point, client_id) => {
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(&point, client_id);
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::MouseHoldMiddle(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| {
tab.handle_mouse_hold_middle(&point, client_id);
});
screen.render();
},

View file

@ -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<String> {
None
}
fn mouse_left_click_release(&self, _position: &Position) -> Option<String> {
None
}
fn mouse_right_click(&self, _position: &Position, _is_held: bool) -> Option<String> {
None
}
fn mouse_right_click_release(&self, _position: &Position) -> Option<String> {
None
}
fn mouse_middle_click(&self, _position: &Position, _is_held: bool) -> Option<String> {
None
}
fn mouse_middle_click_release(&self, _position: &Position) -> Option<String> {
None
}
fn mouse_scroll_up(&self, _position: &Position) -> Option<String> {
None
}
fn mouse_scroll_down(&self, _position: &Position) -> Option<String> {
None
}
fn get_line_number(&self) -> Option<usize> {
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 mut relative_position = active_pane.relative_position(position);
relative_position.change_column(
(relative_position.column())
.max(0)
.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);
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());
relative_position.change_column(
(relative_position.column())
.max(0)
.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}[<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

View file

@ -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<PtyWriteInstruction>,
) -> 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<RefCell<SixelImageStore>>,
@ -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<PtyWriteInstruction> =
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 messages_to_pty_writer = Arc::new(Mutex::new(vec![]));
let (to_pty_writer, pty_writer_receiver): ChannelWithContext<PtyWriteInstruction> =
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}[?1000;1006h"); // normal event tracking (1000) 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
// no hold event here, as hold events are not reported in normal mode
"\u{1b}[<0;75;7m".to_string(), // SGR left button release
"\u{1b}[<2;71;5M".to_string(), // SGR right click
// no hold event here, as hold events are not reported in normal mode
"\u{1b}[<2;75;7m".to_string(), // SGR right button release
"\u{1b}[<1;71;5M".to_string(), // SGR middle click
// no hold event here, as hold events are not reported in normal mode
"\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_utf8_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<PtyWriteInstruction> =
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;1005h"); // button event tracking (1002) with utf8 encoding (1005)
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}[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
]
);
}
#[test]
fn pane_in_utf8_normal_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<PtyWriteInstruction> =
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}[?1000;1005h"); // normal event tracking (1000) 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}[M g%".to_string(), // utf8 left click
// no hold event here, as hold events are not reported in normal mode
"\u{1b}[M#k'".to_string(), // utf8 left button release
"\u{1b}[M\"g%".to_string(), // utf8 right click
// no hold event here, as hold events are not reported in normal mode
"\u{1b}[M#k'".to_string(), // utf8 right button release
"\u{1b}[M!g%".to_string(), // utf8 middle click
// no hold event here, as hold events are not reported in normal mode
"\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
]
);
}

View file

@ -105,6 +105,14 @@ impl ThreadSenders {
self.should_silently_fail = true;
self
}
#[allow(unused)]
pub fn replace_to_pty_writer(
&mut self,
new_pty_writer: SenderWithContext<PtyWriteInstruction>,
) {
// this is mostly used for the tests, see struct
self.to_pty_writer.replace(new_pty_writer);
}
}
/// A container for a receiver, OS input and the senders to a given thread

View file

@ -295,8 +295,13 @@ pub enum ScreenContext {
ChangeMode,
LeftClick,
RightClick,
MouseRelease,
MouseHold,
MiddleClick,
LeftMouseRelease,
RightMouseRelease,
MiddleMouseRelease,
MouseHoldLeft,
MouseHoldRight,
MouseHoldMiddle,
Copy,
ToggleTab,
AddClient,

View file

@ -125,8 +125,13 @@ pub enum Action {
Detach,
LeftClick(Position),
RightClick(Position),
MouseRelease(Position),
MouseHold(Position),
MiddleClick(Position),
LeftMouseRelease(Position),
RightMouseRelease(Position),
MiddleMouseRelease(Position),
MouseHoldLeft(Position),
MouseHoldRight(Position),
MouseHoldMiddle(Position),
Copy,
/// Confirm a prompt
Confirm,

View file

@ -13,6 +13,13 @@ impl Position {
column: Column(column as usize),
}
}
pub fn change_line(&mut self, line: isize) {
self.line = Line(line);
}
pub fn change_column(&mut self, column: usize) {
self.column = Column(column);
}
pub fn relative_to(&self, line: usize, column: usize) -> Self {
Self {