aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/input/mouse_encode.zig
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/libghostty-vt/src/input/mouse_encode.zig')
-rw-r--r--vendor/libghostty-vt/src/input/mouse_encode.zig781
1 files changed, 781 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/input/mouse_encode.zig b/vendor/libghostty-vt/src/input/mouse_encode.zig
new file mode 100644
index 00000000..13a8bd46
--- /dev/null
+++ b/vendor/libghostty-vt/src/input/mouse_encode.zig
@@ -0,0 +1,781 @@
+const std = @import("std");
+const testing = std.testing;
+const terminal = @import("../terminal/main.zig");
+const Terminal = terminal.Terminal;
+const renderer_size = @import("../renderer/size.zig");
+const point = @import("../terminal/point.zig");
+const key = @import("key.zig");
+const mouse = @import("mouse.zig");
+
+const log = std.log.scoped(.mouse_encode);
+
+/// Options that affect mouse encoding behavior and provide runtime context.
+pub const Options = struct {
+ /// Terminal mouse reporting mode (X10, normal, button, any).
+ event: terminal.MouseEvent = .none,
+
+ /// Terminal mouse reporting format.
+ format: terminal.MouseFormat = .x10,
+
+ /// Full renderer size used to convert surface-space pixel positions
+ /// into grid cell coordinates (for most formats) and terminal-space
+ /// pixel coordinates (for SGR-Pixels), as well as to determine
+ /// whether a position falls outside the visible viewport.
+ size: renderer_size.Size,
+
+ /// Whether any mouse button is currently pressed. When a motion
+ /// event occurs outside the viewport, it is only reported if a
+ /// button is held down and the event mode supports motion tracking.
+ /// Without this, out-of-viewport motions are silently dropped.
+ ///
+ /// This should reflect the state of the current event as well, so
+ /// if the encoded event is a button press, this should be true.
+ any_button_pressed: bool = false,
+
+ /// Last reported viewport cell for motion deduplication.
+ /// If null, motion deduplication state is not tracked.
+ last_cell: ?*?point.Coordinate = null,
+
+ /// Initialize from terminal and renderer state. The caller may still
+ /// set any_button_pressed and last_cell on the returned value.
+ pub fn fromTerminal(
+ t: *const Terminal,
+ size: renderer_size.Size,
+ ) Options {
+ return .{
+ .event = t.flags.mouse_event,
+ .format = t.flags.mouse_format,
+ .size = size,
+ };
+ }
+};
+
+/// A normalized mouse event for protocol encoding.
+pub const Event = struct {
+ /// The action of this mouse event.
+ action: mouse.Action = .press,
+
+ /// The button involved in this event. This can be null in the
+ /// case of a motion action with no pressed buttons.
+ button: ?mouse.Button = null,
+
+ /// Keyboard modifiers held during this event.
+ mods: key.Mods = .{},
+
+ /// Mouse position in terminal-space pixels, with (0, 0) at the top-left
+ /// of the terminal. Negative values are allowed and indicate positions
+ /// above or to the left of the terminal. Values larger than the terminal
+ /// size are also allowed and indicate right or below the terminal.
+ pos: Pos = .{},
+
+ /// Mouse position in surface-space pixels.
+ pub const Pos = extern struct {
+ x: f32 = 0,
+ y: f32 = 0,
+ };
+};
+
+/// Encode the mouse event to the writer according to the options.
+///
+/// Not all events result in output.
+pub fn encode(
+ writer: *std.Io.Writer,
+ event: Event,
+ opts: Options,
+) std.Io.Writer.Error!void {
+ if (!shouldReport(event, opts)) return;
+
+ // Handle scenarios where the mouse position is outside the viewport.
+ // We always report release events no matter where they happen.
+ if (event.action != .release and
+ posOutOfViewport(event.pos, opts.size))
+ {
+ // If we don't have a motion-tracking event mode, do nothing,
+ // because events outside the viewport are never reported in
+ // such cases.
+ if (!terminal.mouse.eventSendsMotion(opts.event)) return;
+
+ // For motion modes, we only report if a button is currently pressed.
+ // This lets a TUI detect a click over the surface + drag out
+ // of the surface.
+ if (!opts.any_button_pressed) return;
+ }
+
+ const cell = posToCell(event.pos, opts.size);
+
+ // We only send motion events when the cell changed unless
+ // we're tracking raw pixels.
+ if (event.action == .motion and opts.format != .sgr_pixels) {
+ if (opts.last_cell) |last| {
+ if (last.*) |last_cell| {
+ if (last_cell.eql(cell)) return;
+ }
+ }
+ }
+
+ // Update the last reported cell if we are tracking it.
+ if (opts.last_cell) |last| last.* = cell;
+
+ const button_code = buttonCode(event, opts) orelse return;
+ switch (opts.format) {
+ .x10 => {
+ if (cell.x > 222 or cell.y > 222) {
+ log.info("X10 mouse format can only encode X/Y up to 223", .{});
+ return;
+ }
+
+ // + 1 because our x/y are zero-indexed and the protocol uses 1-indexing.
+ try writer.writeAll("\x1B[M");
+ try writer.writeByte(32 + button_code);
+ try writer.writeByte(32 + @as(u8, @intCast(cell.x)) + 1);
+ try writer.writeByte(32 + @as(u8, @intCast(cell.y)) + 1);
+ },
+
+ .utf8 => {
+ try writer.writeAll("\x1B[M");
+
+ // The button code always fits in a single byte.
+ try writer.writeByte(32 + button_code);
+
+ var buf: [4]u8 = undefined;
+ const x_cp: u21 = @intCast(@as(u32, cell.x) + 33);
+ const y_cp: u21 = @intCast(cell.y + 33);
+
+ const x_len = std.unicode.utf8Encode(x_cp, &buf) catch unreachable;
+ try writer.writeAll(buf[0..x_len]);
+
+ const y_len = std.unicode.utf8Encode(y_cp, &buf) catch unreachable;
+ try writer.writeAll(buf[0..y_len]);
+ },
+
+ .sgr => try writer.print("\x1B[<{d};{d};{d}{c}", .{
+ button_code,
+ cell.x + 1,
+ cell.y + 1,
+ @as(u8, if (event.action == .release) 'm' else 'M'),
+ }),
+
+ .urxvt => try writer.print("\x1B[{d};{d};{d}M", .{
+ 32 + button_code,
+ cell.x + 1,
+ cell.y + 1,
+ }),
+
+ .sgr_pixels => {
+ const pixels = posToPixels(event.pos, opts.size);
+ try writer.print("\x1B[<{d};{d};{d}{c}", .{
+ button_code,
+ pixels.x,
+ pixels.y,
+ @as(u8, if (event.action == .release) 'm' else 'M'),
+ });
+ },
+ }
+}
+
+/// Returns true if this event should be reported for the given mouse
+/// event mode.
+fn shouldReport(event: Event, opts: Options) bool {
+ return switch (opts.event) {
+ .none => false,
+
+ // X10 only reports button presses of left, middle, and right.
+ .x10 => event.action == .press and
+ event.button != null and
+ (event.button.? == .left or
+ event.button.? == .middle or
+ event.button.? == .right),
+
+ // Normal mode does not report motion.
+ .normal => event.action != .motion,
+
+ // Button mode requires an active button for motion events.
+ .button => event.button != null,
+
+ // Any mode reports everything.
+ .any => true,
+ };
+}
+
+fn buttonCode(event: Event, opts: Options) ?u8 {
+ var acc: u8 = code: {
+ if (event.button == null) {
+ // Null button means motion with no pressed button.
+ break :code 3;
+ }
+
+ if (event.action == .release and
+ opts.format != .sgr and
+ opts.format != .sgr_pixels)
+ {
+ // Legacy releases are always encoded as button 3.
+ break :code 3;
+ }
+
+ break :code switch (event.button.?) {
+ .left => 0,
+ .middle => 1,
+ .right => 2,
+ .four => 64,
+ .five => 65,
+ .six => 66,
+ .seven => 67,
+ .eight => 128,
+ .nine => 129,
+ else => return null,
+ };
+ };
+
+ // X10 does not include modifiers.
+ if (opts.event != .x10) {
+ if (event.mods.shift) acc += 4;
+ if (event.mods.alt) acc += 8;
+ if (event.mods.ctrl) acc += 16;
+ }
+
+ // Motion adds another bit.
+ if (event.action == .motion) acc += 32;
+
+ return acc;
+}
+
+/// Terminal-space pixel position for SGR pixel reporting.
+const PixelPoint = struct {
+ x: i32,
+ y: i32,
+};
+
+/// Returns true if the surface-space pixel position is outside the
+/// visible viewport bounds (negative or beyond screen dimensions).
+fn posOutOfViewport(pos: Event.Pos, size: renderer_size.Size) bool {
+ const max_x: f32 = @floatFromInt(size.screen.width);
+ const max_y: f32 = @floatFromInt(size.screen.height);
+ return pos.x < 0 or pos.y < 0 or pos.x > max_x or pos.y > max_y;
+}
+
+/// Converts a surface-space pixel position to a zero-based grid cell
+/// coordinate (column, row) within the terminal viewport. Out-of-bounds
+/// values are clamped to the valid grid range (0 to columns/rows - 1).
+fn posToCell(pos: Event.Pos, size: renderer_size.Size) point.Coordinate {
+ const coord: renderer_size.Coordinate = .{ .surface = .{
+ .x = @as(f64, @floatCast(pos.x)),
+ .y = @as(f64, @floatCast(pos.y)),
+ } };
+ const grid = coord.convert(.grid, size).grid;
+ return .{ .x = grid.x, .y = grid.y };
+}
+
+/// Converts a surface-space pixel position to terminal-space pixel
+/// coordinates (accounting for padding/scaling) used by SGR-Pixels mode.
+/// Unlike grid conversion, terminal-space coordinates are not clamped
+/// and may be negative or exceed the terminal dimensions.
+fn posToPixels(pos: Event.Pos, size: renderer_size.Size) PixelPoint {
+ const coord: renderer_size.Coordinate.Terminal = (renderer_size.Coordinate{ .surface = .{
+ .x = @as(f64, @floatCast(pos.x)),
+ .y = @as(f64, @floatCast(pos.y)),
+ } }).convert(.terminal, size).terminal;
+
+ return .{
+ .x = @as(i32, @intFromFloat(@round(coord.x))),
+ .y = @as(i32, @intFromFloat(@round(coord.y))),
+ };
+}
+
+fn testSize() renderer_size.Size {
+ return .{
+ .screen = .{ .width = 1_000, .height = 1_000 },
+ .cell = .{ .width = 1, .height = 1 },
+ .padding = .{},
+ };
+}
+
+test "shouldReport: none mode never reports" {
+ const size = testSize();
+ inline for ([_]mouse.Action{ .press, .release, .motion }) |action| {
+ try testing.expect(!shouldReport(.{
+ .button = .left,
+ .action = action,
+ }, .{ .event = .none, .size = size }));
+ }
+}
+
+test "shouldReport: x10 reports only left/middle/right press" {
+ const size = testSize();
+ // Left, middle, right presses should report.
+ inline for ([_]mouse.Button{ .left, .middle, .right }) |btn| {
+ try testing.expect(shouldReport(.{
+ .button = btn,
+ .action = .press,
+ }, .{ .event = .x10, .size = size }));
+ }
+
+ // Release is not reported.
+ try testing.expect(!shouldReport(.{
+ .button = .left,
+ .action = .release,
+ }, .{ .event = .x10, .size = size }));
+
+ // Motion is not reported.
+ try testing.expect(!shouldReport(.{
+ .button = .left,
+ .action = .motion,
+ }, .{ .event = .x10, .size = size }));
+
+ // Other buttons are not reported.
+ try testing.expect(!shouldReport(.{
+ .button = .four,
+ .action = .press,
+ }, .{ .event = .x10, .size = size }));
+
+ // Null button is not reported.
+ try testing.expect(!shouldReport(.{
+ .button = null,
+ .action = .press,
+ }, .{ .event = .x10, .size = size }));
+}
+
+test "shouldReport: normal reports press and release but not motion" {
+ const size = testSize();
+ try testing.expect(shouldReport(.{
+ .button = .left,
+ .action = .press,
+ }, .{ .event = .normal, .size = size }));
+
+ try testing.expect(shouldReport(.{
+ .button = .left,
+ .action = .release,
+ }, .{ .event = .normal, .size = size }));
+
+ try testing.expect(!shouldReport(.{
+ .button = .left,
+ .action = .motion,
+ }, .{ .event = .normal, .size = size }));
+}
+
+test "shouldReport: button mode requires a button" {
+ const size = testSize();
+ // With a button, all actions report.
+ inline for ([_]mouse.Action{ .press, .release, .motion }) |action| {
+ try testing.expect(shouldReport(.{
+ .button = .left,
+ .action = action,
+ }, .{ .event = .button, .size = size }));
+ }
+
+ // Without a button (null), nothing reports.
+ inline for ([_]mouse.Action{ .press, .release, .motion }) |action| {
+ try testing.expect(!shouldReport(.{
+ .button = null,
+ .action = action,
+ }, .{ .event = .button, .size = size }));
+ }
+}
+
+test "shouldReport: any mode reports everything" {
+ const size = testSize();
+ inline for ([_]mouse.Action{ .press, .release, .motion }) |action| {
+ try testing.expect(shouldReport(.{
+ .button = .left,
+ .action = action,
+ }, .{ .event = .any, .size = size }));
+ }
+
+ // Even null button + motion reports.
+ try testing.expect(shouldReport(.{
+ .button = null,
+ .action = .motion,
+ }, .{ .event = .any, .size = size }));
+}
+
+test "x10 press left" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .press,
+ .mods = .{ .shift = true, .alt = true, .ctrl = true },
+ .pos = .{ .x = 0, .y = 0 },
+ }, .{
+ .event = .x10,
+ .format = .x10,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualSlices(u8, &.{
+ 0x1B,
+ '[',
+ 'M',
+ 32,
+ 33,
+ 33,
+ }, writer.buffered());
+}
+
+test "x10 ignores release" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .release,
+ }, .{
+ .event = .x10,
+ .format = .x10,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqual(@as(usize, 0), writer.buffered().len);
+}
+
+test "normal ignores motion" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .motion,
+ }, .{
+ .event = .normal,
+ .format = .sgr,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqual(@as(usize, 0), writer.buffered().len);
+}
+
+test "button mode requires button" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = null,
+ .action = .motion,
+ }, .{
+ .event = .button,
+ .format = .sgr,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqual(@as(usize, 0), writer.buffered().len);
+}
+
+test "sgr release keeps button identity" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .right,
+ .action = .release,
+ .pos = .{ .x = 4, .y = 5 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualStrings("\x1B[<2;5;6m", writer.buffered());
+}
+
+test "sgr motion with no button" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = null,
+ .action = .motion,
+ .pos = .{ .x = 1, .y = 2 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualStrings("\x1B[<35;2;3M", writer.buffered());
+}
+
+test "urxvt with modifiers" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .press,
+ .mods = .{ .shift = true, .alt = true, .ctrl = true },
+ .pos = .{ .x = 2, .y = 3 },
+ }, .{
+ .event = .any,
+ .format = .urxvt,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualStrings("\x1B[60;3;4M", writer.buffered());
+}
+
+test "utf8 encodes large coordinates" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .press,
+ .pos = .{ .x = 300, .y = 400 },
+ }, .{
+ .event = .any,
+ .format = .utf8,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ const out = writer.buffered();
+ try testing.expectEqualSlices(u8, &.{ 0x1B, '[', 'M', 32 }, out[0..4]);
+
+ const view = try std.unicode.Utf8View.init(out[4..]);
+ var it = view.iterator();
+ try testing.expectEqual(@as(u21, 333), it.nextCodepoint().?);
+ try testing.expectEqual(@as(u21, 433), it.nextCodepoint().?);
+ try testing.expectEqual(@as(?u21, null), it.nextCodepoint());
+}
+
+test "x10 coordinate limit" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .press,
+ .pos = .{ .x = 223, .y = 0 },
+ }, .{
+ .event = .x10,
+ .format = .x10,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqual(@as(usize, 0), writer.buffered().len);
+}
+
+test "sgr wheel button mappings" {
+ const Case = struct {
+ button: mouse.Button,
+ code: u8,
+ };
+
+ inline for ([_]Case{
+ .{ .button = .four, .code = 64 },
+ .{ .button = .five, .code = 65 },
+ .{ .button = .six, .code = 66 },
+ .{ .button = .seven, .code = 67 },
+ }) |c| {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = c.button,
+ .action = .press,
+ .pos = .{ .x = 0, .y = 0 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ var expected: [32]u8 = undefined;
+ const want = try std.fmt.bufPrint(&expected, "\x1B[<{d};1;1M", .{c.code});
+ try testing.expectEqualStrings(want, writer.buffered());
+ }
+}
+
+test "urxvt release uses legacy button 3 encoding" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .right,
+ .action = .release,
+ .pos = .{ .x = 2, .y = 3 },
+ }, .{
+ .event = .any,
+ .format = .urxvt,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualStrings("\x1B[35;3;4M", writer.buffered());
+}
+
+test "unsupported button is ignored" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .ten,
+ .action = .press,
+ .pos = .{ .x = 1, .y = 1 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqual(@as(usize, 0), writer.buffered().len);
+}
+
+test "sgr pixels uses terminal-space cursor coordinates" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .press,
+ .pos = .{ .x = 10, .y = 20 },
+ }, .{
+ .event = .any,
+ .format = .sgr_pixels,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualStrings("\x1B[<0;10;20M", writer.buffered());
+}
+
+test "sgr pixels release keeps button identity" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .right,
+ .action = .release,
+ .pos = .{ .x = 10, .y = 20 },
+ }, .{
+ .event = .any,
+ .format = .sgr_pixels,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualStrings("\x1B[<2;10;20m", writer.buffered());
+}
+
+test "position exactly at viewport boundary is encoded in final cell" {
+ const size: renderer_size.Size = .{
+ .screen = .{ .width = 10, .height = 10 },
+ .cell = .{ .width = 2, .height = 2 },
+ .padding = .{},
+ };
+
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .press,
+ .pos = .{ .x = 10, .y = 10 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = size,
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualStrings("\x1B[<0;5;5M", writer.buffered());
+}
+
+test "outside viewport motion with no pressed button is ignored" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .motion,
+ .pos = .{ .x = -1, .y = -1 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = testSize(),
+ .any_button_pressed = false,
+ .last_cell = &last,
+ });
+
+ try testing.expectEqual(@as(usize, 0), writer.buffered().len);
+}
+
+test "outside viewport motion with pressed button is reported" {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ var last: ?point.Coordinate = null;
+ try encode(&writer, .{
+ .button = .left,
+ .action = .motion,
+ .pos = .{ .x = -1, .y = -1 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = testSize(),
+ .any_button_pressed = true,
+ .last_cell = &last,
+ });
+
+ try testing.expectEqualStrings("\x1B[<32;1;1M", writer.buffered());
+}
+
+test "motion is deduped by last cell except sgr pixels" {
+ var last: ?point.Coordinate = null;
+
+ {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ try encode(&writer, .{
+ .button = .left,
+ .action = .motion,
+ .pos = .{ .x = 5, .y = 6 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+ try testing.expect(writer.buffered().len > 0);
+ }
+
+ {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ try encode(&writer, .{
+ .button = .left,
+ .action = .motion,
+ .pos = .{ .x = 5, .y = 6 },
+ }, .{
+ .event = .any,
+ .format = .sgr,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+ try testing.expectEqual(@as(usize, 0), writer.buffered().len);
+ }
+
+ {
+ var data: [32]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&data);
+ try encode(&writer, .{
+ .button = .left,
+ .action = .motion,
+ .pos = .{ .x = 5, .y = 6 },
+ }, .{
+ .event = .any,
+ .format = .sgr_pixels,
+ .size = testSize(),
+ .last_cell = &last,
+ });
+ try testing.expect(writer.buffered().len > 0);
+ }
+}