93a94c8d
refactor: share terminal mouse encoding with wheel input
a73x 2026-09-06 10:43
Commit message
src/client/keymap.zig
| Old | New | ||
|---|---|---|---|
| @@ -76,21 +76,20 @@ pub const Event = struct { | |||
| 76 | /// the widest today is the 7-byte modified tilde CSI; the slack is | 76 | /// the widest today is the 7-byte modified tilde CSI; the slack is |
| 77 | /// headroom for forms not in the table yet. | 77 | /// headroom for forms not in the table yet. |
| 78 | pub const max_seq_len = 16; | 78 | pub const max_seq_len = 16; |
| 79 | pub const WheelFormat = enum { x10, utf8, sgr, urxvt, sgr_pixels }; | 79 | pub const MouseFormat = enum { x10, utf8, sgr, urxvt, sgr_pixels }; |
| 80 | pub const wheel_max_seq_len = 64; | 80 | pub const mouse_max_seq_len = 64; |
| 81 | 81 | ||
| 82 | /// Encode one application wheel notch. Button 64 is up and 65 is down; wheel | 82 | /// Encode one semantic mouse event. Coordinates are one-based on the wire. |
| 83 | /// events never emit a button release. Coordinates are one-based on the wire. | 83 | pub fn encodeMouse(format: MouseFormat, button_base: u8, release: bool, cell_x: u16, cell_y: u16, pixel_x: u32, pixel_y: u32, mods: Mods, buf: []u8) []const u8 { |
| 84 | pub fn encodeWheel(format: WheelFormat, up: bool, cell_x: u16, cell_y: u16, pixel_x: u32, pixel_y: u32, mods: Mods, buf: []u8) []const u8 { | 84 | std.debug.assert(buf.len >= mouse_max_seq_len); |
| 85 | std.debug.assert(buf.len >= wheel_max_seq_len); | 85 | const base: u16 = if (release and format != .sgr and format != .sgr_pixels) 3 else button_base; |
| 86 | const base: u16 = if (up) 64 else 65; | ||
| 87 | const button: u16 = base + @as(u16, @intFromBool(mods.shift)) * 4 + @as(u16, @intFromBool(mods.alt)) * 8 + @as(u16, @intFromBool(mods.ctrl)) * 16; | 86 | const button: u16 = base + @as(u16, @intFromBool(mods.shift)) * 4 + @as(u16, @intFromBool(mods.alt)) * 8 + @as(u16, @intFromBool(mods.ctrl)) * 16; |
| 88 | const x: u32 = if (format == .sgr_pixels) pixel_x else cell_x; | 87 | const x: u32 = if (format == .sgr_pixels) pixel_x else cell_x; |
| 89 | const y: u32 = if (format == .sgr_pixels) pixel_y else cell_y; | 88 | const y: u32 = if (format == .sgr_pixels) pixel_y else cell_y; |
| 90 | const bx = x +| 1; | 89 | const bx = x +| 1; |
| 91 | const by = y +| 1; | 90 | const by = y +| 1; |
| 92 | switch (format) { | 91 | switch (format) { |
| 93 | .sgr, .sgr_pixels => return std.fmt.bufPrint(buf, "\x1b[<{d};{d};{d}M", .{ button, bx, by }) catch unreachable, | 92 | .sgr, .sgr_pixels => return std.fmt.bufPrint(buf, "\x1b[<{d};{d};{d}{c}", .{ button, bx, by, @as(u8, if (release) 'm' else 'M') }) catch unreachable, |
| 94 | .urxvt => return std.fmt.bufPrint(buf, "\x1b[{d};{d};{d}M", .{ button + 32, bx, by }) catch unreachable, | 93 | .urxvt => return std.fmt.bufPrint(buf, "\x1b[{d};{d};{d}M", .{ button + 32, bx, by }) catch unreachable, |
| 95 | .x10 => { | 94 | .x10 => { |
| 96 | if (bx > 223 or by > 223) return buf[0..0]; | 95 | if (bx > 223 or by > 223) return buf[0..0]; |
| @@ -116,6 +115,10 @@ pub fn encodeWheel(format: WheelFormat, up: bool, cell_x: u16, cell_y: u16, pixe | |||
| 116 | } | 115 | } |
| 117 | } | 116 | } |
| 118 | 117 | ||
| 118 | pub fn encodeWheel(format: MouseFormat, up: bool, cell_x: u16, cell_y: u16, pixel_x: u32, pixel_y: u32, mods: Mods, buf: []u8) []const u8 { | ||
| 119 | return encodeMouse(format, if (up) 64 else 65, false, cell_x, cell_y, pixel_x, pixel_y, mods, buf); | ||
| 120 | } | ||
| 121 | |||
| 119 | pub fn encodeWheelArrow(up: bool, cursor_keys: bool, buf: []u8) []const u8 { | 122 | pub fn encodeWheelArrow(up: bool, cursor_keys: bool, buf: []u8) []const u8 { |
| 120 | const bytes = encode(.{ .key = if (up) .up else .down }, buf); | 123 | const bytes = encode(.{ .key = if (up) .up else .down }, buf); |
| 121 | if (cursor_keys) buf[1] = 'O'; | 124 | if (cursor_keys) buf[1] = 'O'; |
| @@ -123,7 +126,7 @@ pub fn encodeWheelArrow(up: bool, cursor_keys: bool, buf: []u8) []const u8 { | |||
| 123 | } | 126 | } |
| 124 | 127 | ||
| 125 | test "wheel encodes negotiated SGR and legacy forms" { | 128 | test "wheel encodes negotiated SGR and legacy forms" { |
| 126 | var buf: [wheel_max_seq_len]u8 = undefined; | 129 | var buf: [mouse_max_seq_len]u8 = undefined; |
| 127 | try std.testing.expectEqualStrings("\x1b[<64;4;6M", encodeWheel(.sgr, true, 3, 5, 0, 0, .{}, &buf)); | 130 | try std.testing.expectEqualStrings("\x1b[<64;4;6M", encodeWheel(.sgr, true, 3, 5, 0, 0, .{}, &buf)); |
| 128 | try std.testing.expectEqualStrings("\x1b[97;4;6M", encodeWheel(.urxvt, false, 3, 5, 0, 0, .{}, &buf)); | 131 | try std.testing.expectEqualStrings("\x1b[97;4;6M", encodeWheel(.urxvt, false, 3, 5, 0, 0, .{}, &buf)); |
| 129 | try std.testing.expectEqualStrings("\x1bOA", encodeWheelArrow(true, true, &buf)); | 132 | try std.testing.expectEqualStrings("\x1bOA", encodeWheelArrow(true, true, &buf)); |
| @@ -134,6 +137,10 @@ test "wheel encodes negotiated SGR and legacy forms" { | |||
| 134 | try std.testing.expectEqualSlices(u8, &.{ 27, '[', 'M', 96, 0xdf, 0xbf, 0xdf, 0xbf }, encodeWheel(.utf8, true, 2014, 2014, 0, 0, .{}, &buf)); | 137 | try std.testing.expectEqualSlices(u8, &.{ 27, '[', 'M', 96, 0xdf, 0xbf, 0xdf, 0xbf }, encodeWheel(.utf8, true, 2014, 2014, 0, 0, .{}, &buf)); |
| 135 | try std.testing.expectEqual(@as(usize, 0), encodeWheel(.utf8, true, 2015, 0, 0, 0, .{}, &buf).len); | 138 | try std.testing.expectEqual(@as(usize, 0), encodeWheel(.utf8, true, 2015, 0, 0, 0, .{}, &buf).len); |
| 136 | try std.testing.expectEqualStrings("\x1b[<93;1;1M", encodeWheel(.sgr, false, 0, 0, 0, 0, .{ .shift = true, .alt = true, .ctrl = true }, &buf)); | 139 | try std.testing.expectEqualStrings("\x1b[<93;1;1M", encodeWheel(.sgr, false, 0, 0, 0, 0, .{ .shift = true, .alt = true, .ctrl = true }, &buf)); |
| 140 | try std.testing.expectEqualStrings("\x1b[<1;4;6m", encodeMouse(.sgr, 1, true, 3, 5, 0, 0, .{}, &buf)); | ||
| 141 | try std.testing.expectEqualSlices(u8, &.{ 27, '[', 'M', 51, 33, 33 }, encodeMouse(.x10, 0, true, 0, 0, 0, 0, .{ .ctrl = true }, &buf)); | ||
| 142 | try std.testing.expectEqualStrings("\x1b[35;4;6M", encodeMouse(.urxvt, 2, true, 3, 5, 0, 0, .{}, &buf)); | ||
| 143 | try std.testing.expectEqualStrings("\x1b[<64;4;6M", encodeWheel(.sgr, true, 3, 5, 0, 0, .{}, &buf)); | ||
| 137 | } | 144 | } |
| 138 | 145 | ||
| 139 | /// Encode one event into `buf` (at least max_seq_len bytes), returning the | 146 | /// Encode one event into `buf` (at least max_seq_len bytes), returning the |
src/client/session_pump.zig
| Old | New | ||
|---|---|---|---|
| @@ -489,7 +489,7 @@ pub const Pump = struct { | |||
| 489 | self.selection_revision +%= 1; | 489 | self.selection_revision +%= 1; |
| 490 | self.mu.unlock(); | 490 | self.mu.unlock(); |
| 491 | self.wake(); | 491 | self.wake(); |
| 492 | var seq: [client.keymap.wheel_max_seq_len]u8 = undefined; | 492 | var seq: [client.keymap.mouse_max_seq_len]u8 = undefined; |
| 493 | const bytes = if (modes.appMouse()) client.keymap.encodeWheel( | 493 | const bytes = if (modes.appMouse()) client.keymap.encodeWheel( |
| 494 | if (modes.mouse_sgr_pixels) .sgr_pixels else if (modes.mouse_sgr) .sgr else if (modes.mouse_urxvt) .urxvt else if (modes.mouse_utf8) .utf8 else .x10, | 494 | if (modes.mouse_sgr_pixels) .sgr_pixels else if (modes.mouse_sgr) .sgr else if (modes.mouse_urxvt) .urxvt else if (modes.mouse_utf8) .utf8 else .x10, |
| 495 | wheel.notches > 0, | 495 | wheel.notches > 0, |