aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-27 20:29:49 +0300
committerGitHub <noreply@github.com>2026-09-27 20:29:49 +0300
commit0d1a39dcee6a0f244a036c55935970f40cfa76b8 (patch)
treeb36e41bc3dfb71041e5fa2502c0457e37aea417f /src
parent316d176768e99bd80b81696c4e1f310f0643e744 (diff)
feat: accept mouse events on terminal session control (#4685)
Diffstat (limited to 'src')
-rw-r--r--src/client/terminal_sessions.rs60
-rw-r--r--src/client/tests/mod.rs61
-rw-r--r--src/server/headless/tests/mod.rs46
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) =