diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/c/mouse_encode.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/c/mouse_encode.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/mouse_encode.zig | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/c/mouse_encode.zig b/vendor/libghostty-vt/src/terminal/c/mouse_encode.zig new file mode 100644 index 00000000..331dd606 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/c/mouse_encode.zig @@ -0,0 +1,530 @@ +const 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: GhosttyMouseEncoder +pub const Encoder = ?*MouseEncoderWrapper; + +/// C: GhosttyMouseTrackingMode +pub const TrackingMode = terminal_mouse.Event; + +/// C: GhosttyMouseFormat +pub const Format = terminal_mouse.Format; + +/// C: GhosttyMouseEncoderSize +pub 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: GhosttyMouseEncoderOption +pub 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); +} |
