mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-29 18:41:40 +00:00
libghostty: add tracked grid ref API (#12785)
Add a C API for tracked pins, known as a tracked grid ref in C. The new API can create tracked refs from terminal points, snapshot them back to regular grid refs for cell access, convert them to coordinates, move them to a new point, report when their semantic location was lost, and free the tracked pin bookkeeping. This is backed by PageList tracked pins and exposed through the libghostty-vt export layer and headers.
This commit is contained in:
@@ -228,6 +228,7 @@ const Mouse = struct {
|
||||
/// coordinates so that scrolling preserves the location.
|
||||
left_click_pin: ?*terminal.Pin = null,
|
||||
left_click_screen: terminal.ScreenSet.Key = .primary,
|
||||
left_click_screen_generation: usize = 0,
|
||||
|
||||
/// The starting xpos/ypos of the left click. Note that if scrolling occurs,
|
||||
/// these will point to different "cells", but the xpos/ypos will stay
|
||||
@@ -261,6 +262,22 @@ const Mouse = struct {
|
||||
/// The last x/y in the cursor position for links. We use this to
|
||||
/// only process link hover events when the mouse actually moves cells.
|
||||
link_point: ?terminal.point.Coordinate = null,
|
||||
|
||||
/// Return the PageList that owns the left-click pin, or null if the screen
|
||||
/// has been removed/reinitialized since the pin was tracked.
|
||||
fn leftClickPageList(self: *const Mouse, screens: *const terminal.ScreenSet) ?*terminal.PageList {
|
||||
if (screens.generation(self.left_click_screen) != self.left_click_screen_generation) return null;
|
||||
const screen = screens.get(self.left_click_screen) orelse return null;
|
||||
return &screen.pages;
|
||||
}
|
||||
|
||||
/// Return the left-click pin only if it still belongs to the active screen.
|
||||
fn activeLeftClickPin(self: *const Mouse, screens: *const terminal.ScreenSet) ?*terminal.Pin {
|
||||
const pin = self.left_click_pin orelse return null;
|
||||
if (self.left_click_screen != screens.active_key) return null;
|
||||
_ = self.leftClickPageList(screens) orelse return null;
|
||||
return pin;
|
||||
}
|
||||
};
|
||||
|
||||
/// Keyboard state for the surface.
|
||||
@@ -1192,9 +1209,9 @@ fn selectionScrollTick(self: *Surface) !void {
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const t: *terminal.Terminal = self.renderer_state.terminal;
|
||||
|
||||
// If our screen changed while this is happening, we stop our
|
||||
// selection scroll.
|
||||
if (self.mouse.left_click_screen != t.screens.active_key) {
|
||||
// If our left-click pin no longer belongs to the active screen, we stop
|
||||
// our selection scroll.
|
||||
if (self.mouse.activeLeftClickPin(&t.screens) == null) {
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
@@ -1592,7 +1609,7 @@ fn mouseRefreshLinks(
|
||||
// mouse actions.
|
||||
const left_idx = @intFromEnum(input.MouseButton.left);
|
||||
if (self.mouse.click_state[left_idx] == .press) click: {
|
||||
const pin = self.mouse.left_click_pin orelse break :click;
|
||||
const pin = self.mouse.activeLeftClickPin(&self.io.terminal.screens) orelse break :click;
|
||||
const click_pt = self.io.terminal.screens.active.pages.pointFromPin(
|
||||
.viewport,
|
||||
pin.*,
|
||||
@@ -3927,15 +3944,14 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
|
||||
if (self.mouse.left_click_pin) |prev| {
|
||||
if (t.screens.get(self.mouse.left_click_screen)) |pin_screen| {
|
||||
pin_screen.pages.untrackPin(prev);
|
||||
}
|
||||
if (self.mouse.leftClickPageList(&t.screens)) |pages| pages.untrackPin(prev);
|
||||
self.mouse.left_click_pin = null;
|
||||
}
|
||||
|
||||
// Store it
|
||||
self.mouse.left_click_pin = pin;
|
||||
self.mouse.left_click_screen = t.screens.active_key;
|
||||
self.mouse.left_click_screen_generation = t.screens.generation(t.screens.active_key);
|
||||
self.mouse.left_click_xpos = pos.x;
|
||||
self.mouse.left_click_ypos = pos.y;
|
||||
|
||||
@@ -4466,7 +4482,7 @@ pub fn mousePressureCallback(
|
||||
|
||||
// This should always be set in this state but we don't want
|
||||
// to handle state inconsistency here.
|
||||
const pin = self.mouse.left_click_pin orelse break :select;
|
||||
const pin = self.mouse.activeLeftClickPin(&self.io.terminal.screens) orelse break :select;
|
||||
const sel = self.io.terminal.screens.active.selectWord(
|
||||
pin.*,
|
||||
self.config.selection_word_chars,
|
||||
@@ -4631,11 +4647,12 @@ pub fn cursorPosCallback(
|
||||
// count because we don't want to handle selection.
|
||||
if (self.mouse.left_click_count == 0) break :select;
|
||||
|
||||
// If our terminal screen changed then we don't process this. We don't
|
||||
// invalidate our pin or mouse state because if the screen switches
|
||||
// back then we can continue our selection.
|
||||
// If our left-click pin no longer belongs to the active screen then we
|
||||
// don't process this. We don't invalidate our pin or mouse state
|
||||
// because if the same screen switches back then we can continue our
|
||||
// selection.
|
||||
const t: *terminal.Terminal = self.renderer_state.terminal;
|
||||
if (self.mouse.left_click_screen != t.screens.active_key) break :select;
|
||||
if (self.mouse.activeLeftClickPin(&t.screens) == null) break :select;
|
||||
|
||||
// All roads lead to requiring a re-render at this point.
|
||||
try self.queueRender();
|
||||
@@ -4690,7 +4707,7 @@ fn dragLeftClickDouble(
|
||||
drag_pin: terminal.Pin,
|
||||
) !void {
|
||||
const screen: *terminal.Screen = self.io.terminal.screens.active;
|
||||
const click_pin = self.mouse.left_click_pin.?.*;
|
||||
const click_pin = (self.mouse.activeLeftClickPin(&self.io.terminal.screens) orelse return).*;
|
||||
|
||||
// Get the word closest to our starting click.
|
||||
const word_start = screen.selectWordBetween(
|
||||
@@ -4735,7 +4752,11 @@ fn dragLeftClickTriple(
|
||||
drag_pin: terminal.Pin,
|
||||
) !void {
|
||||
const screen: *terminal.Screen = self.io.terminal.screens.active;
|
||||
const click_pin = self.mouse.left_click_pin.?.*;
|
||||
const click_pin: terminal.Pin = pin: {
|
||||
const set: *terminal.ScreenSet = &self.io.terminal.screens;
|
||||
const tracked = self.mouse.activeLeftClickPin(set) orelse return;
|
||||
break :pin tracked.*;
|
||||
};
|
||||
|
||||
// Get the line selection under our current drag point. If there isn't a
|
||||
// line, do nothing.
|
||||
@@ -4762,8 +4783,13 @@ fn dragLeftClickSingle(
|
||||
drag_x: f64,
|
||||
) !void {
|
||||
// This logic is in a separate function so that it can be unit tested.
|
||||
const click_pin: terminal.Pin = pin: {
|
||||
const set: *terminal.ScreenSet = &self.io.terminal.screens;
|
||||
const tracked = self.mouse.activeLeftClickPin(set) orelse return;
|
||||
break :pin tracked.*;
|
||||
};
|
||||
try self.io.terminal.screens.active.select(mouseSelection(
|
||||
self.mouse.left_click_pin.?.*,
|
||||
click_pin,
|
||||
drag_pin,
|
||||
@intFromFloat(@max(0.0, self.mouse.left_click_xpos)),
|
||||
@intFromFloat(@max(0.0, drag_x)),
|
||||
|
||||
@@ -240,6 +240,7 @@ comptime {
|
||||
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
|
||||
@export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" });
|
||||
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
|
||||
@export(&c.terminal_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" });
|
||||
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
|
||||
@export(&c.kitty_graphics_get, .{ .name = "ghostty_kitty_graphics_get" });
|
||||
@export(&c.kitty_graphics_image, .{ .name = "ghostty_kitty_graphics_image" });
|
||||
@@ -262,6 +263,11 @@ comptime {
|
||||
@export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" });
|
||||
@export(&c.grid_ref_hyperlink_uri, .{ .name = "ghostty_grid_ref_hyperlink_uri" });
|
||||
@export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" });
|
||||
@export(&c.tracked_grid_ref_free, .{ .name = "ghostty_tracked_grid_ref_free" });
|
||||
@export(&c.tracked_grid_ref_has_value, .{ .name = "ghostty_tracked_grid_ref_has_value" });
|
||||
@export(&c.tracked_grid_ref_point, .{ .name = "ghostty_tracked_grid_ref_point" });
|
||||
@export(&c.tracked_grid_ref_set, .{ .name = "ghostty_tracked_grid_ref_set" });
|
||||
@export(&c.tracked_grid_ref_snapshot, .{ .name = "ghostty_tracked_grid_ref_snapshot" });
|
||||
@export(&c.build_info, .{ .name = "ghostty_build_info" });
|
||||
@export(&c.type_json, .{ .name = "ghostty_type_json" });
|
||||
@export(&c.alloc_alloc, .{ .name = "ghostty_alloc" });
|
||||
|
||||
@@ -30,6 +30,11 @@ active: *Screen,
|
||||
/// All screens that are initialized.
|
||||
all: std.EnumMap(Key, *Screen),
|
||||
|
||||
/// Monotonic generation counter for each screen key. This changes whenever a
|
||||
/// screen is removed so external handles can distinguish a newly initialized
|
||||
/// screen from stale references into destroyed screen storage.
|
||||
generations: std.EnumMap(Key, usize),
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
opts: Screen.Options,
|
||||
@@ -42,6 +47,7 @@ pub fn init(
|
||||
.active_key = .primary,
|
||||
.active = screen,
|
||||
.all = .init(.{ .primary = screen }),
|
||||
.generations = .initFull(0),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,6 +65,11 @@ pub fn get(self: *const ScreenSet, key: Key) ?*Screen {
|
||||
return self.all.get(key);
|
||||
}
|
||||
|
||||
/// Get the current generation for the given screen key.
|
||||
pub fn generation(self: *const ScreenSet, key: Key) usize {
|
||||
return self.generations.get(key).?;
|
||||
}
|
||||
|
||||
/// Get the screen for the given key, initializing it if necessary.
|
||||
pub fn getInit(
|
||||
self: *ScreenSet,
|
||||
@@ -82,6 +93,7 @@ pub fn remove(
|
||||
) void {
|
||||
assert(key != .primary);
|
||||
if (self.all.fetchRemove(key)) |screen| {
|
||||
self.generations.put(key, self.generation(key) +% 1);
|
||||
screen.deinit();
|
||||
alloc.destroy(screen);
|
||||
}
|
||||
@@ -99,9 +111,40 @@ test ScreenSet {
|
||||
var set: ScreenSet = try .init(alloc, .default);
|
||||
defer set.deinit(alloc);
|
||||
try testing.expectEqual(.primary, set.active_key);
|
||||
try testing.expectEqual(@as(usize, 0), set.generation(.primary));
|
||||
try testing.expectEqual(@as(usize, 0), set.generation(.alternate));
|
||||
|
||||
// Initialize a secondary screen
|
||||
_ = try set.getInit(alloc, .alternate, .default);
|
||||
try testing.expectEqual(@as(usize, 0), set.generation(.alternate));
|
||||
|
||||
set.switchTo(.alternate);
|
||||
try testing.expectEqual(.alternate, set.active_key);
|
||||
}
|
||||
|
||||
test "ScreenSet generations" {
|
||||
const alloc = testing.allocator;
|
||||
var set: ScreenSet = try .init(alloc, .default);
|
||||
defer set.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), set.generation(.primary));
|
||||
try testing.expectEqual(@as(usize, 0), set.generation(.alternate));
|
||||
|
||||
// A no-op removal doesn't change the generation.
|
||||
set.remove(alloc, .alternate);
|
||||
try testing.expectEqual(@as(usize, 0), set.generation(.alternate));
|
||||
|
||||
// Initializing a screen doesn't change the generation.
|
||||
_ = try set.getInit(alloc, .alternate, .default);
|
||||
try testing.expectEqual(@as(usize, 0), set.generation(.alternate));
|
||||
|
||||
const alternate_generation = set.generation(.alternate);
|
||||
set.remove(alloc, .alternate);
|
||||
try testing.expectEqual(alternate_generation +% 1, set.generation(.alternate));
|
||||
|
||||
// Reinitializing keeps the generation from the last removal, so stale
|
||||
// handles can distinguish the new screen from the destroyed screen.
|
||||
_ = try set.getInit(alloc, .alternate, .default);
|
||||
try testing.expectEqual(alternate_generation +% 1, set.generation(.alternate));
|
||||
try testing.expectEqual(@as(usize, 0), set.generation(.primary));
|
||||
}
|
||||
|
||||
187
src/terminal/c/grid_ref_tracked.zig
Normal file
187
src/terminal/c/grid_ref_tracked.zig
Normal file
@@ -0,0 +1,187 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const lib = @import("../lib.zig");
|
||||
const PageList = @import("../PageList.zig");
|
||||
const point = @import("../point.zig");
|
||||
const grid_ref_c = @import("grid_ref.zig");
|
||||
const terminal_c = @import("terminal.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
/// C: GhosttyTrackedGridRef
|
||||
///
|
||||
/// An owned tracked reference to a position in the terminal grid. The
|
||||
/// underlying PageList pin is automatically updated as the PageList changes.
|
||||
pub const CTrackedGridRef = ?*TrackedGridRef;
|
||||
|
||||
pub const TrackedGridRef = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
terminal: terminal_c.Terminal,
|
||||
screen_key: terminal_c.TerminalScreen,
|
||||
screen_generation: usize,
|
||||
pin: *PageList.Pin,
|
||||
|
||||
/// Return the PageList that owns this tracked ref's pin, or null if the
|
||||
/// owning screen has been removed/reinitialized since the ref was created.
|
||||
fn pageList(ref: *const TrackedGridRef) ?*PageList {
|
||||
const wrapper = ref.terminal orelse return null;
|
||||
const t = wrapper.terminal;
|
||||
if (t.screens.generation(ref.screen_key) != ref.screen_generation) return null;
|
||||
const screen = t.screens.get(ref.screen_key) orelse return null;
|
||||
return &screen.pages;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn tracked_grid_ref_free(ref_: CTrackedGridRef) callconv(lib.calling_conv) void {
|
||||
const ref = ref_ orelse return;
|
||||
if (ref.pageList()) |list| list.untrackPin(ref.pin);
|
||||
ref.alloc.destroy(ref);
|
||||
}
|
||||
|
||||
pub fn tracked_grid_ref_has_value(ref_: CTrackedGridRef) callconv(lib.calling_conv) bool {
|
||||
const ref = ref_ orelse return false;
|
||||
_ = ref.pageList() orelse return false;
|
||||
return !ref.pin.garbage;
|
||||
}
|
||||
|
||||
pub fn tracked_grid_ref_snapshot(
|
||||
ref_: CTrackedGridRef,
|
||||
out_ref: ?*grid_ref_c.CGridRef,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const ref = ref_ orelse return .invalid_value;
|
||||
_ = ref.pageList() orelse return .no_value;
|
||||
if (ref.pin.garbage) return .no_value;
|
||||
if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(ref.pin.*);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn tracked_grid_ref_point(
|
||||
ref_: CTrackedGridRef,
|
||||
tag: point.Tag,
|
||||
out: ?*point.Coordinate,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const ref = ref_ orelse return .invalid_value;
|
||||
const list = ref.pageList() orelse return .no_value;
|
||||
if (ref.pin.garbage) return .no_value;
|
||||
const pt = list.pointFromPin(tag, ref.pin.*) orelse return .no_value;
|
||||
if (out) |o| o.* = pt.coord();
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn tracked_grid_ref_set(
|
||||
ref_: CTrackedGridRef,
|
||||
terminal_: terminal_c.Terminal,
|
||||
pt: point.Point.C,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const ref = ref_ orelse return .invalid_value;
|
||||
const wrapper = terminal_ orelse return .invalid_value;
|
||||
if (ref.terminal != terminal_) return .invalid_value;
|
||||
|
||||
const t = wrapper.terminal;
|
||||
const list = &t.screens.active.pages;
|
||||
const p = list.pin(point.Point.fromC(pt)) orelse return .invalid_value;
|
||||
const tracked_pin = list.trackPin(p) catch return .out_of_memory;
|
||||
|
||||
if (ref.pageList()) |old_list| old_list.untrackPin(ref.pin);
|
||||
ref.screen_key = t.screens.active_key;
|
||||
ref.screen_generation = t.screens.generation(ref.screen_key);
|
||||
ref.pin = tracked_pin;
|
||||
return .success;
|
||||
}
|
||||
|
||||
test "tracked_grid_ref snapshots after terminal scroll" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib.alloc.test_allocator,
|
||||
&terminal,
|
||||
.{ .cols = 5, .rows = 2, .max_scrollback = 10_000 },
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
terminal_c.vt_write(terminal, "A", 1);
|
||||
|
||||
var ref: CTrackedGridRef = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.grid_ref_track(
|
||||
terminal,
|
||||
point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
|
||||
&ref,
|
||||
));
|
||||
defer tracked_grid_ref_free(ref);
|
||||
|
||||
terminal_c.vt_write(terminal, "\r\nB\r\nC", 6);
|
||||
try testing.expect(tracked_grid_ref_has_value(ref));
|
||||
|
||||
var snapshot: grid_ref_c.CGridRef = undefined;
|
||||
try testing.expectEqual(Result.success, tracked_grid_ref_snapshot(ref, &snapshot));
|
||||
|
||||
var buf: [1]u32 = undefined;
|
||||
var len: usize = undefined;
|
||||
try testing.expectEqual(Result.success, grid_ref_c.grid_ref_graphemes(&snapshot, &buf, buf.len, &len));
|
||||
try testing.expectEqual(@as(usize, 1), len);
|
||||
try testing.expectEqual(@as(u32, 'A'), buf[0]);
|
||||
}
|
||||
|
||||
test "tracked_grid_ref reports no value after reset" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib.alloc.test_allocator,
|
||||
&terminal,
|
||||
.{ .cols = 5, .rows = 2, .max_scrollback = 10_000 },
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
terminal_c.vt_write(terminal, "A", 1);
|
||||
|
||||
var ref: CTrackedGridRef = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.grid_ref_track(
|
||||
terminal,
|
||||
point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
|
||||
&ref,
|
||||
));
|
||||
defer tracked_grid_ref_free(ref);
|
||||
|
||||
terminal_c.reset(terminal);
|
||||
try testing.expect(!tracked_grid_ref_has_value(ref));
|
||||
|
||||
var snapshot: grid_ref_c.CGridRef = undefined;
|
||||
try testing.expectEqual(Result.no_value, tracked_grid_ref_snapshot(ref, &snapshot));
|
||||
}
|
||||
|
||||
test "tracked_grid_ref reports no value after alternate screen reset" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib.alloc.test_allocator,
|
||||
&terminal,
|
||||
.{ .cols = 5, .rows = 2, .max_scrollback = 10_000 },
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
terminal_c.vt_write(terminal, "\x1b[?1049hA", 9);
|
||||
|
||||
var ref: CTrackedGridRef = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.grid_ref_track(
|
||||
terminal,
|
||||
point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
|
||||
&ref,
|
||||
));
|
||||
defer tracked_grid_ref_free(ref);
|
||||
|
||||
terminal_c.vt_write(terminal, "\x1bc", 2);
|
||||
try testing.expect(!tracked_grid_ref_has_value(ref));
|
||||
|
||||
var snapshot: grid_ref_c.CGridRef = undefined;
|
||||
try testing.expectEqual(Result.no_value, tracked_grid_ref_snapshot(ref, &snapshot));
|
||||
|
||||
var coord: point.Coordinate = undefined;
|
||||
try testing.expectEqual(Result.no_value, tracked_grid_ref_point(ref, .active, &coord));
|
||||
|
||||
terminal_c.vt_write(terminal, "\x1b[?1049h", 8);
|
||||
try testing.expect(!tracked_grid_ref_has_value(ref));
|
||||
|
||||
try testing.expectEqual(Result.success, tracked_grid_ref_set(
|
||||
ref,
|
||||
terminal,
|
||||
point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
|
||||
));
|
||||
try testing.expect(tracked_grid_ref_has_value(ref));
|
||||
try testing.expectEqual(Result.success, tracked_grid_ref_snapshot(ref, &snapshot));
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub const color = @import("color.zig");
|
||||
pub const focus = @import("focus.zig");
|
||||
pub const formatter = @import("formatter.zig");
|
||||
pub const grid_ref = @import("grid_ref.zig");
|
||||
pub const grid_ref_tracked = @import("grid_ref_tracked.zig");
|
||||
pub const kitty_graphics = @import("kitty_graphics.zig");
|
||||
pub const kitty_graphics_get = kitty_graphics.get;
|
||||
pub const kitty_graphics_image = kitty_graphics.image_get_handle;
|
||||
@@ -170,6 +171,7 @@ pub const terminal_mode_set = terminal.mode_set;
|
||||
pub const terminal_get = terminal.get;
|
||||
pub const terminal_get_multi = terminal.get_multi;
|
||||
pub const terminal_grid_ref = terminal.grid_ref;
|
||||
pub const terminal_grid_ref_track = terminal.grid_ref_track;
|
||||
pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref;
|
||||
|
||||
pub const type_json = types.get_json;
|
||||
@@ -179,6 +181,11 @@ pub const grid_ref_row = grid_ref.grid_ref_row;
|
||||
pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes;
|
||||
pub const grid_ref_hyperlink_uri = grid_ref.grid_ref_hyperlink_uri;
|
||||
pub const grid_ref_style = grid_ref.grid_ref_style;
|
||||
pub const tracked_grid_ref_free = grid_ref_tracked.tracked_grid_ref_free;
|
||||
pub const tracked_grid_ref_has_value = grid_ref_tracked.tracked_grid_ref_has_value;
|
||||
pub const tracked_grid_ref_point = grid_ref_tracked.tracked_grid_ref_point;
|
||||
pub const tracked_grid_ref_set = grid_ref_tracked.tracked_grid_ref_set;
|
||||
pub const tracked_grid_ref_snapshot = grid_ref_tracked.tracked_grid_ref_snapshot;
|
||||
|
||||
test {
|
||||
_ = allocator;
|
||||
@@ -186,6 +193,7 @@ test {
|
||||
_ = cell;
|
||||
_ = color;
|
||||
_ = grid_ref;
|
||||
_ = grid_ref_tracked;
|
||||
_ = kitty_graphics;
|
||||
_ = row;
|
||||
_ = focus;
|
||||
|
||||
@@ -19,6 +19,7 @@ const size_report = @import("../size_report.zig");
|
||||
const cell_c = @import("cell.zig");
|
||||
const row_c = @import("row.zig");
|
||||
const grid_ref_c = @import("grid_ref.zig");
|
||||
const grid_ref_tracked_c = @import("grid_ref_tracked.zig");
|
||||
const style_c = @import("style.zig");
|
||||
const color = @import("../color.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
@@ -723,18 +724,44 @@ pub fn grid_ref(
|
||||
out_ref: ?*grid_ref_c.CGridRef,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
|
||||
const zig_pt: point.Point = switch (pt.tag) {
|
||||
.active => .{ .active = pt.value.active },
|
||||
.viewport => .{ .viewport = pt.value.viewport },
|
||||
.screen => .{ .screen = pt.value.screen },
|
||||
.history => .{ .history = pt.value.history },
|
||||
};
|
||||
const zig_pt: point.Point = .fromC(pt);
|
||||
const p = t.screens.active.pages.pin(zig_pt) orelse
|
||||
return .invalid_value;
|
||||
if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(p);
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn grid_ref_track(
|
||||
terminal_: Terminal,
|
||||
pt: point.Point.C,
|
||||
out_ref: ?*grid_ref_tracked_c.CTrackedGridRef,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const wrapper = terminal_ orelse return .invalid_value;
|
||||
const out = out_ref orelse return .invalid_value;
|
||||
out.* = null;
|
||||
|
||||
const t: *ZigTerminal = wrapper.terminal;
|
||||
const list = &t.screens.active.pages;
|
||||
const p = list.pin(.fromC(pt)) orelse return .invalid_value;
|
||||
const tracked_pin = list.trackPin(p) catch return .out_of_memory;
|
||||
|
||||
const alloc = t.gpa();
|
||||
const ref = alloc.create(grid_ref_tracked_c.TrackedGridRef) catch {
|
||||
list.untrackPin(tracked_pin);
|
||||
return .out_of_memory;
|
||||
};
|
||||
ref.* = .{
|
||||
.alloc = alloc,
|
||||
.terminal = wrapper,
|
||||
.screen_key = t.screens.active_key,
|
||||
.screen_generation = t.screens.generation(t.screens.active_key),
|
||||
.pin = tracked_pin,
|
||||
};
|
||||
|
||||
out.* = ref;
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn point_from_grid_ref(
|
||||
terminal_: Terminal,
|
||||
ref: *const grid_ref_c.CGridRef,
|
||||
|
||||
@@ -76,6 +76,16 @@ pub const Point = union(Tag) {
|
||||
pub const C = c_union.C;
|
||||
pub const CValue = c_union.CValue;
|
||||
pub const cval = c_union.cval;
|
||||
|
||||
/// Convert a C ABI point into the native Zig tagged union.
|
||||
pub fn fromC(pt: C) Point {
|
||||
return switch (pt.tag) {
|
||||
.active => .{ .active = pt.value.active },
|
||||
.viewport => .{ .viewport = pt.value.viewport },
|
||||
.screen => .{ .screen = pt.value.screen },
|
||||
.history => .{ .history = pt.value.history },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Coordinate = extern struct {
|
||||
|
||||
Reference in New Issue
Block a user