diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-27 20:29:49 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-27 20:29:49 +0300 |
| commit | 0d1a39dcee6a0f244a036c55935970f40cfa76b8 (patch) | |
| tree | b36e41bc3dfb71041e5fa2502c0457e37aea417f /src | |
| parent | 316d176768e99bd80b81696c4e1f310f0643e744 (diff) | |
feat: accept mouse events on terminal session control (#4685)
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/terminal_sessions.rs | 60 | ||||
| -rw-r--r-- | src/client/tests/mod.rs | 61 | ||||
| -rw-r--r-- | src/server/headless/tests/mod.rs | 46 |
3 files changed, 165 insertions, 2 deletions
diff --git a/src/client/terminal_sessions.rs b/src/client/terminal_sessions.rs index 6d3160c8..aeecbf47 100644 --- a/src/client/terminal_sessions.rs +++ b/src/client/terminal_sessions.rs @@ -7,8 +7,8 @@ use tracing::info; use crate::ipc::LocalStream; use crate::protocol::{ - self, AttachScrollDirection, AttachScrollSource, ClientMessage, RenderEncoding, ServerMessage, - MAX_GRAPHICS_FRAME_SIZE, + self, AttachScrollDirection, AttachScrollSource, ClientMessage, ClientMouseButton, + ClientMouseKind, ClientMousePosition, RenderEncoding, ServerMessage, MAX_GRAPHICS_FRAME_SIZE, }; use crate::server::socket_paths::client_socket_path; @@ -187,12 +187,40 @@ enum TerminalControlCommand { #[serde(default)] modifiers: u8, }, + #[serde(rename = "terminal.mouse")] + Mouse { + action: TerminalControlMouseAction, + #[serde(default)] + button: TerminalControlMouseButton, + column: u16, + row: u16, + #[serde(default)] + modifiers: u8, + }, #[serde(rename = "terminal.release")] Release {}, } #[derive(Clone, Copy, serde::Deserialize)] #[serde(rename_all = "snake_case")] +enum TerminalControlMouseAction { + Down, + Up, + Drag, + Move, +} + +#[derive(Clone, Copy, Default, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +enum TerminalControlMouseButton { + #[default] + Left, + Right, + Middle, +} + +#[derive(Clone, Copy, serde::Deserialize)] +#[serde(rename_all = "snake_case")] enum TerminalControlScrollDirection { Up, Down, @@ -273,6 +301,34 @@ pub(super) fn terminal_control_command_from_json(raw: &str) -> Result<ClientMess modifiers, }) } + TerminalControlCommand::Mouse { + action, + button, + column, + row, + modifiers, + } => { + let button = match button { + TerminalControlMouseButton::Left => ClientMouseButton::Left, + TerminalControlMouseButton::Right => ClientMouseButton::Right, + TerminalControlMouseButton::Middle => ClientMouseButton::Middle, + }; + let kind = match action { + TerminalControlMouseAction::Down => ClientMouseKind::Down(button), + TerminalControlMouseAction::Up => ClientMouseKind::Up(button), + TerminalControlMouseAction::Drag => ClientMouseKind::Drag(button), + TerminalControlMouseAction::Move => ClientMouseKind::Moved, + }; + // The server encodes the event for the pane's mouse mode and drops it + // when the application has not enabled mouse reporting. + Ok(ClientMessage::AttachMouse { + kind, + position: ClientMousePosition::Cell { column, row }, + geometry: None, + modifiers, + lines: 1, + }) + } TerminalControlCommand::Release {} => Ok(ClientMessage::Detach), } } diff --git a/src/client/tests/mod.rs b/src/client/tests/mod.rs index 8519ea07..2fcac6eb 100644 --- a/src/client/tests/mod.rs +++ b/src/client/tests/mod.rs @@ -887,6 +887,67 @@ fn terminal_control_resize_command_maps_to_client_resize() { } #[test] +fn terminal_control_mouse_command_maps_to_attach_mouse() { + let action = terminal_control_command_from_json( + r#"{"type":"terminal.mouse","action":"down","column":12,"row":5}"#, + ) + .unwrap(); + let ClientMessage::AttachMouse { + kind, + position, + geometry, + modifiers, + lines, + } = action + else { + panic!("expected attach mouse command"); + }; + assert_eq!( + kind, + crate::protocol::ClientMouseKind::Down(crate::protocol::ClientMouseButton::Left) + ); + assert_eq!( + position, + crate::protocol::ClientMousePosition::Cell { column: 12, row: 5 } + ); + assert_eq!((geometry, modifiers, lines), (None, 0, 1)); +} + +#[test] +fn terminal_control_mouse_command_maps_every_action_and_button() { + use crate::protocol::{ClientMouseButton as Button, ClientMouseKind as Kind}; + for (json, expected) in [ + (r#""action":"up","button":"right""#, Kind::Up(Button::Right)), + ( + r#""action":"drag","button":"middle""#, + Kind::Drag(Button::Middle), + ), + (r#""action":"move""#, Kind::Moved), + ] { + let raw = format!(r#"{{"type":"terminal.mouse",{json},"column":0,"row":0,"modifiers":4}}"#); + let ClientMessage::AttachMouse { + kind, modifiers, .. + } = terminal_control_command_from_json(&raw).unwrap() + else { + panic!("expected attach mouse command"); + }; + assert_eq!((kind, modifiers), (expected, 4), "{raw}"); + } +} + +#[test] +fn terminal_control_mouse_command_rejects_unknown_actions_and_missing_cells() { + assert!(terminal_control_command_from_json( + r#"{"type":"terminal.mouse","action":"click","column":1,"row":1}"# + ) + .is_err()); + assert!(terminal_control_command_from_json( + r#"{"type":"terminal.mouse","action":"down","column":1}"# + ) + .is_err()); +} + +#[test] fn terminal_control_scroll_command_maps_to_attach_scroll() { let action = terminal_control_command_from_json( r#"{"type":"terminal.scroll","direction":"up","lines":3}"#, diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs index 34258dd3..4def3fa4 100644 --- a/src/server/headless/tests/mod.rs +++ b/src/server/headless/tests/mod.rs @@ -6310,6 +6310,52 @@ fn direct_terminal_mouse_uses_runtime_protocol_encoding() { } #[test] +fn direct_terminal_mouse_sends_nothing_without_mouse_reporting() { + with_terminal_session_test_server(|server, runtime_terminal_id, terminal_id, _pane_id| { + // A plain shell never enabled mouse reporting, so a controller's click + // must not reach it as stray escape bytes. + let (runtime, mut input_rx) = + crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes( + 80, 24, 0, b"", 4, + ); + server + .app + .terminal_runtimes + .insert(runtime_terminal_id, runtime); + server.clients.insert( + 1, + ClientConnection::new_with_mode( + ClientConnectionMode::TerminalAttach { + terminal_id: terminal_id.clone(), + }, + (80, 24), + crate::kitty_graphics::HostCellSize::default(), + 1, + RenderEncoding::TerminalAnsi, + None, + ), + ); + + for kind in [ + protocol::ClientMouseKind::Down(protocol::ClientMouseButton::Left), + protocol::ClientMouseKind::Drag(protocol::ClientMouseButton::Left), + protocol::ClientMouseKind::Up(protocol::ClientMouseButton::Left), + protocol::ClientMouseKind::Moved, + ] { + server.handle_server_event(ServerEvent::ClientAttachMouse { + client_id: 1, + kind, + position: protocol::ClientMousePosition::Cell { column: 10, row: 5 }, + geometry: None, + modifiers: 0, + lines: 1, + }); + } + assert!(input_rx.try_recv().is_err()); + }); +} + +#[test] fn direct_terminal_pixel_mouse_uses_runtime_tracking_and_coordinates() { with_terminal_session_test_server(|server, runtime_terminal_id, terminal_id, _pane_id| { let (runtime, mut input_rx) = |
