Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531const std = @import("std");const Allocator = std.mem.Allocator;const testing = std.testing;const lib = @import("../lib.zig");const CAllocator = lib.alloc.Allocator;const input_mouse_encode = @import("../../input/mouse_encode.zig");const renderer_size = @import("../../renderer/size.zig");const point = @import("../point.zig");const terminal_mouse = @import("../mouse.zig");const mouse_event = @import("mouse_event.zig");const Result = @import("result.zig").Result;const Event = mouse_event.Event;const Terminal = @import("terminal.zig").Terminal;const ZigTerminal = @import("../Terminal.zig");
const log = std.log.scoped(.mouse_encode);
/// Wrapper around mouse encoding options that tracks the allocator for C API usage.const MouseEncoderWrapper = struct { opts: input_mouse_encode.Options, track_last_cell: bool = false, last_cell: ?point.Coordinate = null, alloc: Allocator,};
/// C: GhosttyMouseEncoderpub const Encoder = ?*MouseEncoderWrapper;
/// C: GhosttyMouseTrackingModepub const TrackingMode = terminal_mouse.Event;
/// C: GhosttyMouseFormatpub const Format = terminal_mouse.Format;
/// C: GhosttyMouseEncoderSizepub const Size = extern struct { size: usize = @sizeOf(Size), screen_width: u32, screen_height: u32, cell_width: u32, cell_height: u32, padding_top: u32, padding_bottom: u32, padding_right: u32, padding_left: u32,
fn toRenderer(self: Size) ?renderer_size.Size { if (self.cell_width == 0 or self.cell_height == 0) return null; return .{ .screen = .{ .width = self.screen_width, .height = self.screen_height, }, .cell = .{ .width = self.cell_width, .height = self.cell_height, }, .padding = .{ .top = self.padding_top, .bottom = self.padding_bottom, .right = self.padding_right, .left = self.padding_left, }, }; }};
/// C: GhosttyMouseEncoderOptionpub const Option = enum(c_int) { event = 0, format = 1, size = 2, any_button_pressed = 3, track_last_cell = 4,
/// Input type expected for setting the option. pub fn InType(comptime self: Option) type { return switch (self) { .event => TrackingMode, .format => Format, .size => Size, .any_button_pressed, .track_last_cell, => bool, }; }};
pub fn new( alloc_: ?*const CAllocator, result: *Encoder,) callconv(lib.calling_conv) Result { const alloc = lib.alloc.default(alloc_); const ptr = alloc.create(MouseEncoderWrapper) catch return .out_of_memory; ptr.* = .{ .opts = .{ .size = defaultSize() }, .alloc = alloc, }; result.* = ptr; return .success;}
pub fn free(encoder_: Encoder) callconv(lib.calling_conv) void { const wrapper = encoder_ orelse return; const alloc = wrapper.alloc; alloc.destroy(wrapper);}
pub fn setopt( encoder_: Encoder, option: Option, value: ?*const anyopaque,) callconv(lib.calling_conv) void { if (comptime std.debug.runtime_safety) { _ = std.meta.intToEnum(Option, @intFromEnum(option)) catch { log.warn("setopt invalid option value={d}", .{@intFromEnum(option)}); return; }; }
return switch (option) { inline else => |comptime_option| setoptTyped( encoder_, comptime_option, @ptrCast(@alignCast(value orelse return)), ), };}
fn setoptTyped( encoder_: Encoder, comptime option: Option, value: *const option.InType(),) void { const wrapper = encoder_.?; switch (option) { .event => { if (comptime std.debug.runtime_safety) { _ = std.meta.intToEnum(TrackingMode, @intFromEnum(value.*)) catch { log.warn("setopt invalid TrackingMode value={d}", .{@intFromEnum(value.*)}); return; }; }
if (wrapper.opts.event != value.*) wrapper.last_cell = null; wrapper.opts.event = value.*; },
.format => { if (comptime std.debug.runtime_safety) { _ = std.meta.intToEnum(Format, @intFromEnum(value.*)) catch { log.warn("setopt invalid Format value={d}", .{@intFromEnum(value.*)}); return; }; }
if (wrapper.opts.format != value.*) wrapper.last_cell = null; wrapper.opts.format = value.*; },
.size => { if (value.size < @sizeOf(Size)) { log.warn("setopt size struct too small size={d} expected>={d}", .{ value.size, @sizeOf(Size), }); return; }
wrapper.opts.size = value.toRenderer() orelse { log.warn("setopt invalid size values (cell width and height must be non-zero)", .{}); return; }; wrapper.last_cell = null; },
.any_button_pressed => wrapper.opts.any_button_pressed = value.*,
.track_last_cell => { wrapper.track_last_cell = value.*; if (!value.*) wrapper.last_cell = null; }, }}
pub fn setopt_from_terminal( encoder_: Encoder, terminal_: Terminal,) callconv(lib.calling_conv) void { const wrapper = encoder_ orelse return; const t: *ZigTerminal = (terminal_ orelse return).terminal; wrapper.opts.event = t.flags.mouse_event; wrapper.opts.format = t.flags.mouse_format; wrapper.last_cell = null;}
pub fn reset(encoder_: Encoder) callconv(lib.calling_conv) void { const wrapper = encoder_ orelse return; wrapper.last_cell = null;}
pub fn encode( encoder_: Encoder, event_: Event, out_: ?[*]u8, out_len: usize, out_written: *usize,) callconv(lib.calling_conv) Result { const wrapper = encoder_ orelse return .invalid_value; const event = event_ orelse return .invalid_value;
const prev_last_cell = wrapper.last_cell;
var opts = wrapper.opts; opts.last_cell = if (wrapper.track_last_cell) &wrapper.last_cell else null;
var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{}); input_mouse_encode.encode( &writer, event.event, opts, ) catch |err| switch (err) { error.WriteFailed => { // Failed writes should not mutate motion dedupe state because no // complete sequence was produced. wrapper.last_cell = prev_last_cell;
// Use a discarding writer to count how much space we would have needed. var count_last_cell = prev_last_cell; var count_opts = wrapper.opts; count_opts.last_cell = if (wrapper.track_last_cell) &count_last_cell else null;
var discarding: std.Io.Writer.Discarding = .init(&.{}); input_mouse_encode.encode( &discarding.writer, event.event, count_opts, ) catch unreachable;
// Discarding always uses a u64. If we're on 32-bit systems // we cast down. We should make this safer in the future. out_written.* = @intCast(discarding.count); return .out_of_space; }, };
out_written.* = writer.end; return .success;}
fn defaultSize() renderer_size.Size { return .{ .screen = .{ .width = 1, .height = 1 }, .cell = .{ .width = 1, .height = 1 }, .padding = .{}, };}
fn testSize() Size { return .{ .size = @sizeOf(Size), .screen_width = 1_000, .screen_height = 1_000, .cell_width = 1, .cell_height = 1, .padding_top = 0, .padding_bottom = 0, .padding_right = 0, .padding_left = 0, };}
test "alloc" { var e: Encoder = undefined; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &e, )); free(e);}
test "setopt" { var e: Encoder = undefined; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &e, )); defer free(e);
const event_mode: TrackingMode = .any; setopt(e, .event, &event_mode); try testing.expectEqual(TrackingMode.any, e.?.opts.event);
const format_mode: Format = .sgr; setopt(e, .format, &format_mode); try testing.expectEqual(Format.sgr, e.?.opts.format);
const size = testSize(); setopt(e, .size, &size); try testing.expectEqual(size.screen_width, e.?.opts.size.screen.width); try testing.expectEqual(size.screen_height, e.?.opts.size.screen.height);
const any_button_pressed = true; setopt(e, .any_button_pressed, &any_button_pressed); try testing.expect(e.?.opts.any_button_pressed);
const track_last_cell = true; setopt(e, .track_last_cell, &track_last_cell); try testing.expect(e.?.track_last_cell);}
test "setopt_from_terminal" { const terminal_c = @import("terminal.zig");
var e: Encoder = undefined; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &e, )); defer free(e);
var t: Terminal = undefined; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, )); defer terminal_c.free(t);
const event_mode: TrackingMode = .any; setopt(e, .event, &event_mode); const format_mode: Format = .sgr; setopt(e, .format, &format_mode);
setopt_from_terminal(e, t); try testing.expectEqual(TrackingMode.none, e.?.opts.event); try testing.expectEqual(Format.x10, e.?.opts.format);}
test "setopt_from_terminal null" { setopt_from_terminal(null, null);
const terminal_c = @import("terminal.zig"); var t: Terminal = undefined; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, )); defer terminal_c.free(t); setopt_from_terminal(null, t);
var e: Encoder = undefined; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &e, )); defer free(e); setopt_from_terminal(e, null);}
test "encode: sgr press left" { var encoder: Encoder = undefined; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &encoder, )); defer free(encoder);
const event_mode: TrackingMode = .any; setopt(encoder, .event, &event_mode); const format_mode: Format = .sgr; setopt(encoder, .format, &format_mode); const size = testSize(); setopt(encoder, .size, &size);
var event: Event = undefined; try testing.expectEqual(Result.success, mouse_event.new( &lib.alloc.test_allocator, &event, )); defer mouse_event.free(event);
mouse_event.set_action(event, .press); mouse_event.set_button(event, .left); mouse_event.set_position(event, .{ .x = 0, .y = 0 });
var required: usize = 0; try testing.expectEqual(Result.out_of_space, encode( encoder, event, null, 0, &required, )); try testing.expectEqual(@as(usize, 9), required);
var buf: [32]u8 = undefined; var written: usize = 0; try testing.expectEqual(Result.success, encode( encoder, event, &buf, buf.len, &written, )); try testing.expectEqual(required, written); try testing.expectEqualStrings("\x1b[<0;1;1M", buf[0..written]);}
test "encode: motion dedupe and reset" { var encoder: Encoder = undefined; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &encoder, )); defer free(encoder);
const event_mode: TrackingMode = .any; setopt(encoder, .event, &event_mode); const format_mode: Format = .sgr; setopt(encoder, .format, &format_mode); const size = testSize(); setopt(encoder, .size, &size); const track_last_cell = true; setopt(encoder, .track_last_cell, &track_last_cell);
var event: Event = undefined; try testing.expectEqual(Result.success, mouse_event.new( &lib.alloc.test_allocator, &event, )); defer mouse_event.free(event);
mouse_event.set_action(event, .motion); mouse_event.set_button(event, .left); mouse_event.set_position(event, .{ .x = 5, .y = 6 });
{ var buf: [32]u8 = undefined; var written: usize = 0; try testing.expectEqual(Result.success, encode( encoder, event, &buf, buf.len, &written, )); try testing.expect(written > 0); }
{ var buf: [32]u8 = undefined; var written: usize = 0; try testing.expectEqual(Result.success, encode( encoder, event, &buf, buf.len, &written, )); try testing.expectEqual(@as(usize, 0), written); }
reset(encoder);
{ var buf: [32]u8 = undefined; var written: usize = 0; try testing.expectEqual(Result.success, encode( encoder, event, &buf, buf.len, &written, )); try testing.expect(written > 0); }}
test "encode: querying required size doesn't update dedupe state" { var encoder: Encoder = undefined; try testing.expectEqual(Result.success, new( &lib.alloc.test_allocator, &encoder, )); defer free(encoder);
const event_mode: TrackingMode = .any; setopt(encoder, .event, &event_mode); const format_mode: Format = .sgr; setopt(encoder, .format, &format_mode); const size = testSize(); setopt(encoder, .size, &size); const track_last_cell = true; setopt(encoder, .track_last_cell, &track_last_cell);
var event: Event = undefined; try testing.expectEqual(Result.success, mouse_event.new( &lib.alloc.test_allocator, &event, )); defer mouse_event.free(event);
mouse_event.set_action(event, .motion); mouse_event.set_button(event, .left); mouse_event.set_position(event, .{ .x = 5, .y = 6 });
var required: usize = 0; try testing.expectEqual(Result.out_of_space, encode( encoder, event, null, 0, &required, )); try testing.expect(required > 0);
var buf: [32]u8 = undefined; var written: usize = 0; try testing.expectEqual(Result.success, encode( encoder, event, &buf, buf.len, &written, )); try testing.expect(written > 0);}