a73x

93a94c8d

refactor: share terminal mouse encoding with wheel input

a73x   2026-09-06 10:43

Commit message
refactor: share terminal mouse encoding with wheel input

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,