aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/c/mouse_encode.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/c/mouse_encode.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (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.zig530
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);
+}