aboutsummaryrefslogtreecommitdiff
path: root/src/server/pane_input.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/pane_input.rs')
-rw-r--r--src/server/pane_input.rs515
1 files changed, 515 insertions, 0 deletions
diff --git a/src/server/pane_input.rs b/src/server/pane_input.rs
new file mode 100644
index 00000000..4eb64cab
--- /dev/null
+++ b/src/server/pane_input.rs
@@ -0,0 +1,515 @@
+use bytes::Bytes;
+use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseEventKind};
+
+use crate::protocol::{AttachScrollDirection, AttachScrollSource, ClientPaneInputEvent};
+
+pub(super) fn downgrade_ineligible_pixel_mouse(
+ events: &mut [ClientPaneInputEvent],
+ pixel_mouse: bool,
+ runtime_size: (u16, u16),
+ runtime_pixels: Option<(u32, u32)>,
+) {
+ let (runtime_rows, runtime_cols) = runtime_size;
+ for event in events {
+ let ClientPaneInputEvent::Mouse {
+ position, geometry, ..
+ } = event
+ else {
+ continue;
+ };
+ let crate::protocol::ClientMousePosition::Pixels { x, y, column, row } = *position else {
+ continue;
+ };
+ let exact = pixel_mouse
+ && geometry.is_some_and(|geometry| {
+ (runtime_rows, runtime_cols) == (geometry.rows, geometry.cols)
+ && runtime_pixels == Some((geometry.width_px, geometry.height_px))
+ && column < geometry.cols
+ && row < geometry.rows
+ && x > 0
+ && y > 0
+ && x <= geometry.width_px
+ && y <= geometry.height_px
+ });
+ if !exact {
+ *position = crate::protocol::ClientMousePosition::Cell { column, row };
+ *geometry = None;
+ }
+ }
+}
+
+pub(super) fn terminal_attach_mouse_position(
+ runtime: &crate::terminal::TerminalRuntime,
+ terminal_size: (u16, u16),
+ cell_size: crate::kitty_graphics::HostCellSize,
+ pixel_mouse: bool,
+ host_sgr_pixels_active: bool,
+ position: crate::protocol::ClientMousePosition,
+ geometry: Option<crate::protocol::ClientMouseGeometry>,
+) -> Option<crate::protocol::ClientMousePosition> {
+ let runtime_size = runtime.current_size();
+ let cell_fallback = |column, row| {
+ (column < runtime_size.1 && row < runtime_size.0)
+ .then_some(crate::protocol::ClientMousePosition::Cell { column, row })
+ };
+ let (x, y, column, row) = match position {
+ crate::protocol::ClientMousePosition::Cell { column, row } => {
+ return cell_fallback(column, row);
+ }
+ crate::protocol::ClientMousePosition::Pixels { x, y, column, row } => (x, y, column, row),
+ };
+ let Some(geometry) = geometry else {
+ return cell_fallback(column, row);
+ };
+ let host_geometry = crate::input::mouse::HostGeometry::new(
+ geometry.cols,
+ geometry.rows,
+ geometry.width_px,
+ geometry.height_px,
+ )?;
+ if host_geometry.cell(x, y) != Some((column, row)) {
+ return None;
+ }
+ let exact = (|| {
+ let average_width = (geometry.width_px / u32::from(geometry.cols)).max(1);
+ let average_height = (geometry.height_px / u32::from(geometry.rows)).max(1);
+ let (child_width_px, child_height_px) = runtime.pixel_size()?;
+ if !pixel_mouse
+ || !host_sgr_pixels_active
+ || !runtime.sgr_pixel_mouse_enabled()
+ || terminal_size != (geometry.cols, geometry.rows)
+ || runtime_size != (geometry.rows, geometry.cols)
+ || !cell_size.is_known()
+ || average_width != cell_size.width_px
+ || average_height != cell_size.height_px
+ {
+ return None;
+ }
+ let crate::input::mouse::Position::Pixels { x, y } = (crate::input::mouse::HostPixels {
+ x,
+ y,
+ geometry: host_geometry,
+ })
+ .pane_position(
+ ratatui::layout::Rect::new(0, 0, geometry.cols, geometry.rows),
+ child_width_px,
+ child_height_px,
+ )?
+ else {
+ return None;
+ };
+ Some(crate::protocol::ClientMousePosition::Pixels { x, y, column, row })
+ })();
+ exact.or_else(|| cell_fallback(column, row))
+}
+
+pub(super) fn apply_terminal_attach_scroll(
+ runtime: &crate::terminal::TerminalRuntime,
+ source: AttachScrollSource,
+ direction: AttachScrollDirection,
+ lines: u16,
+ column: Option<u16>,
+ row: Option<u16>,
+ modifiers: u8,
+) -> Result<(), String> {
+ apply_scroll(
+ runtime,
+ source,
+ direction,
+ lines,
+ crate::input::mouse::Position::Cell {
+ column: column.unwrap_or(0),
+ row: row.unwrap_or(0),
+ },
+ modifiers,
+ )
+}
+
+fn apply_scroll(
+ runtime: &crate::terminal::TerminalRuntime,
+ source: AttachScrollSource,
+ direction: AttachScrollDirection,
+ lines: u16,
+ position: crate::input::mouse::Position,
+ modifiers: u8,
+) -> Result<(), String> {
+ let wheel_kind = match direction {
+ AttachScrollDirection::Up => MouseEventKind::ScrollUp,
+ AttachScrollDirection::Down => MouseEventKind::ScrollDown,
+ };
+ if let AttachScrollSource::PageKey { input } = source {
+ let host_scroll = runtime
+ .plain_page_keys_use_host_scrollback()
+ .unwrap_or(false);
+ if host_scroll {
+ match direction {
+ AttachScrollDirection::Up => runtime.scroll_up(lines.max(1) as usize),
+ AttachScrollDirection::Down => runtime.scroll_down(lines.max(1) as usize),
+ }
+ return Ok(());
+ }
+ return apply_terminal_attach_input(runtime, input);
+ }
+
+ match runtime.wheel_routing() {
+ Some(crate::pane::WheelRouting::MouseReport) => {
+ runtime.scroll_reset();
+ let Some(bytes) = runtime.encode_mouse_wheel(
+ wheel_kind,
+ position,
+ KeyModifiers::from_bits_truncate(modifiers),
+ ) else {
+ return Err(format!(
+ "failed to encode terminal attach mouse wheel event: {wheel_kind:?}"
+ ));
+ };
+ runtime
+ .try_send_bytes(Bytes::from(bytes))
+ .map_err(|err| format!("terminal attach mouse wheel input failed: {err}"))?;
+ }
+ Some(crate::pane::WheelRouting::AlternateScroll) => {
+ runtime.scroll_reset();
+ let Some(bytes) = runtime.encode_alternate_scroll(wheel_kind) else {
+ return Ok(());
+ };
+ runtime
+ .try_send_bytes(Bytes::from(bytes))
+ .map_err(|err| format!("terminal attach alternate scroll input failed: {err}"))?;
+ }
+ Some(crate::pane::WheelRouting::HostScroll) | None => match direction {
+ AttachScrollDirection::Up => runtime.scroll_up(lines.max(1) as usize),
+ AttachScrollDirection::Down => runtime.scroll_down(lines.max(1) as usize),
+ },
+ }
+ Ok(())
+}
+
+pub(super) fn apply_terminal_attach_input(
+ runtime: &crate::terminal::TerminalRuntime,
+ data: Vec<u8>,
+) -> Result<(), String> {
+ runtime.scroll_reset();
+ if let Some(text) = crate::raw_input::complete_text_bracketed_paste(&data) {
+ runtime
+ .try_send_paste(text.to_owned())
+ .map_err(|err| format!("terminal attach paste failed: {err}"))
+ } else {
+ runtime
+ .try_send_bytes(Bytes::from(data))
+ .map_err(|err| format!("terminal attach input failed: {err}"))
+ }
+}
+
+pub(super) fn apply_client_pane_input_events(
+ runtime: &crate::terminal::TerminalRuntime,
+ events: &[ClientPaneInputEvent],
+) -> Result<(), String> {
+ apply_client_terminal_input_events(runtime, events, true)
+}
+
+pub(super) fn apply_client_popup_input_events(
+ runtime: &crate::terminal::TerminalRuntime,
+ events: &[ClientPaneInputEvent],
+) -> Result<(), String> {
+ apply_client_terminal_input_events(runtime, events, false)
+}
+
+fn apply_client_terminal_input_events(
+ runtime: &crate::terminal::TerminalRuntime,
+ events: &[ClientPaneInputEvent],
+ host_page_keys: bool,
+) -> Result<(), String> {
+ for event in events {
+ if let ClientPaneInputEvent::Mouse {
+ kind,
+ position,
+ modifiers,
+ lines,
+ ..
+ } = event
+ {
+ let kind = kind.to_crossterm();
+ let modifiers = KeyModifiers::from_bits_truncate(*modifiers);
+ let position = match position {
+ crate::protocol::ClientMousePosition::Cell { column, row } => {
+ crate::input::mouse::Position::Cell {
+ column: *column,
+ row: *row,
+ }
+ }
+ crate::protocol::ClientMousePosition::Pixels { x, y, column, row } => {
+ if runtime.sgr_pixel_mouse_enabled() {
+ crate::input::mouse::Position::Pixels { x: *x, y: *y }
+ } else {
+ crate::input::mouse::Position::Cell {
+ column: *column,
+ row: *row,
+ }
+ }
+ }
+ };
+ let bytes = match kind {
+ MouseEventKind::ScrollUp | MouseEventKind::ScrollDown => {
+ let direction = if kind == MouseEventKind::ScrollUp {
+ AttachScrollDirection::Up
+ } else {
+ AttachScrollDirection::Down
+ };
+ apply_scroll(
+ runtime,
+ AttachScrollSource::Wheel,
+ direction,
+ (*lines).max(1),
+ position,
+ modifiers.bits(),
+ )?;
+ continue;
+ }
+ MouseEventKind::ScrollLeft | MouseEventKind::ScrollRight => runtime
+ .encode_mouse_wheel(kind, position, modifiers)
+ .unwrap_or_default(),
+ MouseEventKind::Down(_) | MouseEventKind::Up(_) | MouseEventKind::Drag(_) => {
+ runtime
+ .encode_mouse_button(kind, position, modifiers)
+ .unwrap_or_default()
+ }
+ MouseEventKind::Moved => runtime
+ .encode_mouse_motion(kind, position, modifiers)
+ .unwrap_or_default(),
+ };
+ if !bytes.is_empty() {
+ if kind != MouseEventKind::Moved {
+ runtime.scroll_reset();
+ }
+ runtime
+ .try_send_bytes(Bytes::from(bytes))
+ .map_err(|err| format!("targeted pane mouse input failed: {err}"))?;
+ }
+ continue;
+ }
+
+ match event.to_raw_input_event() {
+ crate::raw_input::RawInputEvent::Key(key) => {
+ let key_event = key.as_key_event();
+ if host_page_keys
+ && matches!(key_event.code, KeyCode::PageUp | KeyCode::PageDown)
+ && key_event.modifiers.is_empty()
+ && runtime.plain_page_keys_use_host_scrollback() == Some(true)
+ {
+ match key_event.kind {
+ KeyEventKind::Release => continue,
+ KeyEventKind::Press | KeyEventKind::Repeat => {
+ let lines = runtime.current_size().0.max(1) as usize;
+ if key_event.code == KeyCode::PageUp {
+ runtime.scroll_up(lines);
+ } else {
+ runtime.scroll_down(lines);
+ }
+ continue;
+ }
+ }
+ }
+
+ runtime.scroll_reset();
+ let bytes = runtime.encode_terminal_key(key);
+ if !bytes.is_empty() {
+ runtime
+ .try_send_bytes(Bytes::from(bytes))
+ .map_err(|err| format!("targeted pane key input failed: {err}"))?;
+ }
+ }
+ crate::raw_input::RawInputEvent::Text(text) => {
+ runtime.scroll_reset();
+ runtime
+ .try_send_bytes(Bytes::copy_from_slice(text.as_str().as_bytes()))
+ .map_err(|err| format!("targeted pane text input failed: {err}"))?;
+ }
+ crate::raw_input::RawInputEvent::Paste(text) => {
+ runtime.scroll_reset();
+ runtime
+ .try_send_paste(text)
+ .map_err(|err| format!("targeted pane paste failed: {err}"))?;
+ }
+ crate::raw_input::RawInputEvent::Mouse(_)
+ | crate::raw_input::RawInputEvent::OuterFocusGained
+ | crate::raw_input::RawInputEvent::OuterFocusLost
+ | crate::raw_input::RawInputEvent::HostDefaultColor { .. }
+ | crate::raw_input::RawInputEvent::HostPaletteColors { .. }
+ | crate::raw_input::RawInputEvent::HostColorSchemeChanged(_)
+ | crate::raw_input::RawInputEvent::HostCellSizeReport { .. }
+ | crate::raw_input::RawInputEvent::Unsupported => {
+ return Err("non-pane input reached targeted pane input".to_owned());
+ }
+ }
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn terminal_attach_stale_geometry_falls_back_to_the_canonical_cell() {
+ let runtime = crate::terminal::TerminalRuntime::test_with_screen_bytes(20, 5, b"");
+ let position = crate::protocol::ClientMousePosition::Pixels {
+ x: 121,
+ y: 81,
+ column: 12,
+ row: 4,
+ };
+
+ assert_eq!(
+ terminal_attach_mouse_position(
+ &runtime,
+ (20, 5),
+ crate::kitty_graphics::HostCellSize {
+ width_px: 10,
+ height_px: 20,
+ },
+ true,
+ false,
+ position,
+ Some(crate::protocol::ClientMouseGeometry {
+ cols: 20,
+ rows: 5,
+ width_px: 200,
+ height_px: 100,
+ }),
+ ),
+ Some(crate::protocol::ClientMousePosition::Cell { column: 12, row: 4 })
+ );
+ assert_eq!(
+ terminal_attach_mouse_position(
+ &runtime,
+ (20, 5),
+ crate::kitty_graphics::HostCellSize {
+ width_px: 10,
+ height_px: 20,
+ },
+ true,
+ false,
+ crate::protocol::ClientMousePosition::Pixels {
+ x: 120,
+ y: 80,
+ column: 12,
+ row: 4,
+ },
+ Some(crate::protocol::ClientMouseGeometry {
+ cols: 20,
+ rows: 5,
+ width_px: 200,
+ height_px: 100,
+ }),
+ ),
+ None
+ );
+ assert_eq!(
+ terminal_attach_mouse_position(
+ &runtime,
+ (80, 24),
+ crate::kitty_graphics::HostCellSize::default(),
+ false,
+ false,
+ crate::protocol::ClientMousePosition::Cell { column: 12, row: 4 },
+ None,
+ ),
+ Some(crate::protocol::ClientMousePosition::Cell { column: 12, row: 4 })
+ );
+ }
+
+ #[test]
+ fn ineligible_shell_pixel_mouse_uses_its_canonical_cell_position() {
+ let mut events = vec![ClientPaneInputEvent::Mouse {
+ kind: crate::protocol::ClientMouseKind::Down(crate::protocol::ClientMouseButton::Left),
+ position: crate::protocol::ClientMousePosition::Pixels {
+ x: 121,
+ y: 81,
+ column: 12,
+ row: 4,
+ },
+ geometry: Some(crate::protocol::ClientMouseGeometry {
+ cols: 20,
+ rows: 5,
+ width_px: 200,
+ height_px: 100,
+ }),
+ modifiers: 0,
+ lines: 1,
+ }];
+
+ downgrade_ineligible_pixel_mouse(&mut events, false, (5, 20), Some((200, 100)));
+
+ assert!(matches!(
+ events.as_slice(),
+ [ClientPaneInputEvent::Mouse {
+ position: crate::protocol::ClientMousePosition::Cell { column: 12, row: 4 },
+ ..
+ }]
+ ));
+ }
+
+ #[test]
+ fn eligible_shell_pixel_mouse_remains_exact() {
+ let position = crate::protocol::ClientMousePosition::Pixels {
+ x: 121,
+ y: 81,
+ column: 12,
+ row: 4,
+ };
+ let mut events = vec![ClientPaneInputEvent::Mouse {
+ kind: crate::protocol::ClientMouseKind::Moved,
+ position,
+ geometry: Some(crate::protocol::ClientMouseGeometry {
+ cols: 20,
+ rows: 5,
+ width_px: 200,
+ height_px: 100,
+ }),
+ modifiers: 0,
+ lines: 1,
+ }];
+
+ downgrade_ineligible_pixel_mouse(&mut events, true, (5, 20), Some((200, 100)));
+
+ assert!(matches!(
+ events.as_slice(),
+ [ClientPaneInputEvent::Mouse {
+ position: current,
+ ..
+ }] if *current == position
+ ));
+ }
+
+ #[test]
+ fn stale_shell_pixel_geometry_downgrades_to_its_canonical_cell() {
+ let mut events = vec![ClientPaneInputEvent::Mouse {
+ kind: crate::protocol::ClientMouseKind::Moved,
+ position: crate::protocol::ClientMousePosition::Pixels {
+ x: 121,
+ y: 81,
+ column: 12,
+ row: 4,
+ },
+ geometry: Some(crate::protocol::ClientMouseGeometry {
+ cols: 20,
+ rows: 5,
+ width_px: 200,
+ height_px: 100,
+ }),
+ modifiers: 0,
+ lines: 1,
+ }];
+
+ downgrade_ineligible_pixel_mouse(&mut events, true, (6, 20), Some((200, 120)));
+
+ assert!(matches!(
+ events.as_slice(),
+ [ClientPaneInputEvent::Mouse {
+ position: crate::protocol::ClientMousePosition::Cell { column: 12, row: 4 },
+ geometry: None,
+ ..
+ }]
+ ));
+ }
+}