aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/c/selection.zig
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:51 +0300
commit5bd21b7ed468b5ff4d1a5b2ed7e211f401eb138a (patch)
tree672fa4625cca0ecb3e9376fc7f8e33d7b0e9deef /vendor/libghostty-vt/src/terminal/c/selection.zig
parent32abb3776668ee768200019c170956f67fe33a86 (diff)
chore: update libghostty vendor
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/c/selection.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/c/selection.zig515
1 files changed, 515 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/c/selection.zig b/vendor/libghostty-vt/src/terminal/c/selection.zig
index 74e96598..cb574ecc 100644
--- a/vendor/libghostty-vt/src/terminal/c/selection.zig
+++ b/vendor/libghostty-vt/src/terminal/c/selection.zig
@@ -1,5 +1,20 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const formatterpkg = @import("../formatter.zig");
const grid_ref = @import("grid_ref.zig");
+const point = @import("../point.zig");
+const selection_codepoints = @import("../selection_codepoints.zig");
const Selection = @import("../Selection.zig");
+const Result = @import("result.zig").Result;
+const terminal_c = @import("terminal.zig");
+
+const log = std.log.scoped(.selection_c);
+
+pub const Adjustment = Selection.Adjustment;
+pub const Order = Selection.Order;
+pub const Format = formatterpkg.Format;
/// C: GhosttySelection
pub const CSelection = extern struct {
@@ -13,4 +28,504 @@ pub const CSelection = extern struct {
const end_pin = self.end.toPin() orelse return null;
return Selection.init(start_pin, end_pin, self.rectangle);
}
+
+ pub fn fromZig(sel: Selection) CSelection {
+ return .{
+ .start = .fromPin(sel.start()),
+ .end = .fromPin(sel.end()),
+ .rectangle = sel.rectangle,
+ };
+ }
+};
+
+/// C: GhosttyTerminalSelectWordOptions
+pub const SelectWordOptions = extern struct {
+ size: usize = @sizeOf(SelectWordOptions),
+ ref: grid_ref.CGridRef,
+ boundary_codepoints: ?[*]const u32 = null,
+ boundary_codepoints_len: usize = 0,
+};
+
+/// C: GhosttyTerminalSelectWordBetweenOptions
+pub const SelectWordBetweenOptions = extern struct {
+ size: usize = @sizeOf(SelectWordBetweenOptions),
+ start: grid_ref.CGridRef,
+ end: grid_ref.CGridRef,
+ boundary_codepoints: ?[*]const u32 = null,
+ boundary_codepoints_len: usize = 0,
};
+
+/// C: GhosttyTerminalSelectLineOptions
+pub const SelectLineOptions = extern struct {
+ size: usize = @sizeOf(SelectLineOptions),
+ ref: grid_ref.CGridRef,
+ whitespace: ?[*]const u32 = null,
+ whitespace_len: usize = 0,
+ semantic_prompt_boundary: bool = false,
+};
+
+/// C: GhosttyTerminalSelectionFormatOptions
+pub const FormatOptions = extern struct {
+ size: usize = @sizeOf(FormatOptions),
+ emit: Format,
+ unwrap: bool,
+ trim: bool,
+ selection: ?*const CSelection = null,
+};
+
+pub fn word(
+ terminal: terminal_c.Terminal,
+ options: ?*const SelectWordOptions,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const opts = options orelse return .invalid_value;
+ if (opts.size < @sizeOf(SelectWordOptions)) return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ const boundary_codepoints = codepointSlice(
+ opts.boundary_codepoints,
+ opts.boundary_codepoints_len,
+ ) catch return .invalid_value;
+
+ const screen = t.screens.active;
+ const pin = opts.ref.toPin() orelse return .invalid_value;
+ out.* = .fromZig(screen.selectWord(
+ pin,
+ boundary_codepoints orelse &selection_codepoints.default_word_boundaries,
+ ) orelse
+ return .no_value);
+ return .success;
+}
+
+pub fn word_between(
+ terminal: terminal_c.Terminal,
+ options: ?*const SelectWordBetweenOptions,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const opts = options orelse return .invalid_value;
+ if (opts.size < @sizeOf(SelectWordBetweenOptions)) return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ const boundary_codepoints = codepointSlice(
+ opts.boundary_codepoints,
+ opts.boundary_codepoints_len,
+ ) catch return .invalid_value;
+
+ const screen = t.screens.active;
+ const start = opts.start.toPin() orelse return .invalid_value;
+ const end = opts.end.toPin() orelse return .invalid_value;
+ out.* = .fromZig(screen.selectWordBetween(
+ start,
+ end,
+ boundary_codepoints orelse &selection_codepoints.default_word_boundaries,
+ ) orelse
+ return .no_value);
+ return .success;
+}
+
+pub fn line(
+ terminal: terminal_c.Terminal,
+ options: ?*const SelectLineOptions,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const opts = options orelse return .invalid_value;
+ if (opts.size < @sizeOf(SelectLineOptions)) return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ const whitespace = codepointSlice(
+ opts.whitespace,
+ opts.whitespace_len,
+ ) catch return .invalid_value;
+
+ const screen = t.screens.active;
+ const pin = opts.ref.toPin() orelse return .invalid_value;
+ out.* = .fromZig(screen.selectLine(.{
+ .pin = pin,
+ .whitespace = whitespace orelse &selection_codepoints.default_line_whitespace,
+ .semantic_prompt_boundary = opts.semantic_prompt_boundary,
+ }) orelse return .no_value);
+ return .success;
+}
+
+pub fn all(
+ terminal: terminal_c.Terminal,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ out.* = .fromZig(t.screens.active.selectAll() orelse return .no_value);
+ return .success;
+}
+
+pub fn output(
+ terminal: terminal_c.Terminal,
+ ref: grid_ref.CGridRef,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ const screen = t.screens.active;
+ const pin = ref.toPin() orelse return .invalid_value;
+ out.* = .fromZig(screen.selectOutput(pin) orelse return .no_value);
+ return .success;
+}
+
+pub fn format_buf(
+ terminal: terminal_c.Terminal,
+ opts: FormatOptions,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+
+ if (out_ == null) {
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ formatSelection(t, opts, &discarding.writer) catch |err| return switch (err) {
+ error.InvalidValue => .invalid_value,
+ error.NoValue => .no_value,
+ error.WriteFailed => unreachable,
+ };
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ }
+
+ var writer: std.Io.Writer = .fixed(out_.?[0..out_len]);
+ formatSelection(t, opts, &writer) catch |err| switch (err) {
+ error.InvalidValue => return .invalid_value,
+ error.NoValue => return .no_value,
+ error.WriteFailed => {
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ formatSelection(t, opts, &discarding.writer) catch unreachable;
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ },
+ };
+
+ out_written.* = writer.end;
+ return .success;
+}
+
+pub fn format_alloc(
+ terminal: terminal_c.Terminal,
+ alloc_: ?*const CAllocator,
+ opts: FormatOptions,
+ out_ptr: *?[*]u8,
+ out_len: *usize,
+) callconv(lib.calling_conv) Result {
+ out_ptr.* = null;
+ out_len.* = 0;
+
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const alloc = lib.alloc.default(alloc_);
+
+ var aw: std.Io.Writer.Allocating = .init(alloc);
+ defer aw.deinit();
+
+ formatSelection(t, opts, &aw.writer) catch |err| return switch (err) {
+ error.InvalidValue => .invalid_value,
+ error.NoValue => .no_value,
+ error.WriteFailed => .out_of_memory,
+ };
+
+ const buf = aw.toOwnedSlice() catch return .out_of_memory;
+ out_ptr.* = buf.ptr;
+ out_len.* = buf.len;
+ return .success;
+}
+
+fn formatSelection(
+ t: *terminal_c.ZigTerminal,
+ opts: FormatOptions,
+ writer: *std.Io.Writer,
+) error{ InvalidValue, NoValue, WriteFailed }!void {
+ var formatter = selectionFormatter(t, opts) catch |err| return err;
+ try formatter.format(writer);
+}
+
+fn selectionFormatter(
+ t: *terminal_c.ZigTerminal,
+ opts: FormatOptions,
+) error{ InvalidValue, NoValue }!formatterpkg.TerminalFormatter {
+ if (opts.size < @sizeOf(FormatOptions)) return error.InvalidValue;
+ _ = std.meta.intToEnum(Format, @intFromEnum(opts.emit)) catch
+ return error.InvalidValue;
+
+ const sel = if (opts.selection) |sel|
+ sel.toZig() orelse return error.InvalidValue
+ else
+ t.screens.active.selection orelse return error.NoValue;
+
+ var formatter: formatterpkg.TerminalFormatter = .init(t, .{
+ .emit = opts.emit,
+ .unwrap = opts.unwrap,
+ .trim = opts.trim,
+ });
+ formatter.content = .{ .selection = sel };
+ return formatter;
+}
+
+/// Return the borrowed C array of `uint32_t` codepoints as a `[]const u21`.
+///
+/// `NULL + len 0` returns null, which callers treat as “use the API default
+/// set.” A non-null pointer with `len 0` returns an empty slice, meaning “use an
+/// explicitly empty set.” A non-zero length requires a non-null pointer.
+///
+/// This is intentionally zero-copy. In the C ABI, codepoints are `uint32_t`,
+/// but selection internals use Zig's `u21` to represent valid Unicode scalar
+/// values. Zig currently stores `u21` in the same size and alignment as `u32`,
+/// so we assert that layout relationship and reinterpret the borrowed slice.
+/// If Zig ever changes that representation, these comptime assertions fail
+/// loudly rather than silently making this cast wrong.
+fn codepointSlice(
+ ptr: ?[*]const u32,
+ len: usize,
+) error{InvalidValue}!?[]const u21 {
+ comptime {
+ std.debug.assert(@sizeOf(u21) == @sizeOf(u32));
+ std.debug.assert(@alignOf(u21) == @alignOf(u32));
+ }
+
+ if (len == 0) {
+ const p = ptr orelse return null;
+ _ = p;
+ return &.{};
+ }
+
+ const p = ptr orelse return error.InvalidValue;
+ const cps: [*]const u21 = @ptrCast(p);
+ return cps[0..len];
+}
+
+pub fn adjust(
+ terminal: terminal_c.Terminal,
+ selection: ?*CSelection,
+ adjustment: Selection.Adjustment,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Selection.Adjustment, @intFromEnum(adjustment)) catch {
+ log.warn("terminal_selection_adjust invalid adjustment value={d}", .{@intFromEnum(adjustment)});
+ return .invalid_value;
+ };
+ }
+
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel_ptr = selection orelse return .invalid_value;
+ var sel = sel_ptr.toZig() orelse return .invalid_value;
+ sel.adjust(t.screens.active, adjustment);
+ sel_ptr.* = .fromZig(sel);
+ return .success;
+}
+
+pub fn order(
+ terminal: terminal_c.Terminal,
+ selection: ?*const CSelection,
+ out_order: ?*Selection.Order,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel = (selection orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const out = out_order orelse return .invalid_value;
+
+ out.* = sel.order(t.screens.active);
+ return .success;
+}
+
+pub fn ordered(
+ terminal: terminal_c.Terminal,
+ selection: ?*const CSelection,
+ desired: Selection.Order,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Selection.Order, @intFromEnum(desired)) catch {
+ log.warn("terminal_selection_ordered invalid desired value={d}", .{@intFromEnum(desired)});
+ return .invalid_value;
+ };
+ }
+
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel = (selection orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ out.* = .fromZig(sel.ordered(t.screens.active, desired));
+ return .success;
+}
+
+pub fn contains(
+ terminal: terminal_c.Terminal,
+ selection: ?*const CSelection,
+ pt: point.Point.C,
+ out_contains: ?*bool,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel = (selection orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const out = out_contains orelse return .invalid_value;
+
+ const screen = t.screens.active;
+ const pin = screen.pages.pin(.fromC(pt)) orelse return .invalid_value;
+ out.* = sel.contains(screen, pin);
+ return .success;
+}
+
+pub fn equal(
+ terminal: terminal_c.Terminal,
+ a: ?*const CSelection,
+ b: ?*const CSelection,
+ out_equal: ?*bool,
+) callconv(lib.calling_conv) Result {
+ _ = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel_a = (a orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const sel_b = (b orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const out = out_equal orelse return .invalid_value;
+
+ out.* = sel_a.eql(sel_b);
+ return .success;
+}
+
+test "selection_format_alloc uses active selection" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Hello World", 11);
+
+ var start_ref: grid_ref.CGridRef = .{};
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 6, .y = 0 } },
+ }, &start_ref));
+
+ var end_ref: grid_ref.CGridRef = .{};
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 10, .y = 0 } },
+ }, &end_ref));
+
+ const sel: CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ };
+ try testing.expectEqual(Result.success, terminal_c.set(t, .selection, @ptrCast(&sel)));
+
+ const opts: FormatOptions = .{
+ .emit = .plain,
+ .unwrap = true,
+ .trim = true,
+ };
+
+ var required: usize = 0;
+ try testing.expectEqual(Result.out_of_space, format_buf(
+ t,
+ opts,
+ null,
+ 0,
+ &required,
+ ));
+ try testing.expectEqual(@as(usize, 5), required);
+
+ var out_ptr: ?[*]u8 = null;
+ var out_len: usize = 0;
+ try testing.expectEqual(Result.success, format_alloc(
+ t,
+ &lib.alloc.test_allocator,
+ opts,
+ &out_ptr,
+ &out_len,
+ ));
+ const ptr = out_ptr orelse return error.TestExpectedEqual;
+ defer lib.alloc.default(&lib.alloc.test_allocator).free(ptr[0..out_len]);
+
+ try testing.expectEqualStrings("World", ptr[0..out_len]);
+}
+
+test "selection_format_buf uses provided selection" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Hello World", 11);
+
+ var start_ref: grid_ref.CGridRef = .{};
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 0, .y = 0 } },
+ }, &start_ref));
+
+ var end_ref: grid_ref.CGridRef = .{};
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 4, .y = 0 } },
+ }, &end_ref));
+
+ const sel: CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ };
+ const opts: FormatOptions = .{
+ .emit = .plain,
+ .unwrap = true,
+ .trim = true,
+ .selection = &sel,
+ };
+
+ var small: [2]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.out_of_space, format_buf(
+ t,
+ opts,
+ &small,
+ small.len,
+ &written,
+ ));
+ try testing.expectEqual(@as(usize, 5), written);
+
+ var buf: [32]u8 = undefined;
+ try testing.expectEqual(Result.success, format_buf(
+ t,
+ opts,
+ &buf,
+ buf.len,
+ &written,
+ ));
+ try testing.expectEqualStrings("Hello", buf[0..written]);
+}
+
+test "selection_format_alloc returns no_value without active selection" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ var out_ptr: ?[*]u8 = @ptrFromInt(1);
+ var out_len: usize = 123;
+ try testing.expectEqual(Result.no_value, format_alloc(
+ t,
+ &lib.alloc.test_allocator,
+ .{ .emit = .plain, .unwrap = true, .trim = true },
+ &out_ptr,
+ &out_len,
+ ));
+ try testing.expect(out_ptr == null);
+ try testing.expectEqual(@as(usize, 0), out_len);
+}