aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-07 18:29:21 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-07 18:29:21 +0300
commitdcfe11a7588fcd42471159453810abc9b7720a57 (patch)
treec32a77266028e73ca2382b96c96f8438a5fcd621 /vendor/libghostty-vt/src/terminal
parent2dfed860103c4e4505ac7f25def9fe37da1e1e6a (diff)
build: update libghostty-vt vendor
vendor ghostty 063ac3e and remove the musl libc++ link workaround. expose the new selection formatter and hyperlink uri apis for follow-up rendering work. add manual artifact validation for static linux builds.
Diffstat (limited to 'vendor/libghostty-vt/src/terminal')
-rw-r--r--vendor/libghostty-vt/src/terminal/PageList.zig11
-rw-r--r--vendor/libghostty-vt/src/terminal/Screen.zig92
-rw-r--r--vendor/libghostty-vt/src/terminal/Terminal.zig91
-rw-r--r--vendor/libghostty-vt/src/terminal/apc.zig76
-rw-r--r--vendor/libghostty-vt/src/terminal/build_options.zig40
-rw-r--r--vendor/libghostty-vt/src/terminal/c/AGENTS.md7
-rw-r--r--vendor/libghostty-vt/src/terminal/c/build_info.zig18
-rw-r--r--vendor/libghostty-vt/src/terminal/c/cell.zig60
-rw-r--r--vendor/libghostty-vt/src/terminal/c/formatter.zig58
-rw-r--r--vendor/libghostty-vt/src/terminal/c/grid_ref.zig96
-rw-r--r--vendor/libghostty-vt/src/terminal/c/key_encode.zig7
-rw-r--r--vendor/libghostty-vt/src/terminal/c/kitty_graphics.zig1710
-rw-r--r--vendor/libghostty-vt/src/terminal/c/main.zig34
-rw-r--r--vendor/libghostty-vt/src/terminal/c/render.zig197
-rw-r--r--vendor/libghostty-vt/src/terminal/c/row.zig61
-rw-r--r--vendor/libghostty-vt/src/terminal/c/selection.zig16
-rw-r--r--vendor/libghostty-vt/src/terminal/c/sys.zig430
-rw-r--r--vendor/libghostty-vt/src/terminal/c/terminal.zig346
-rw-r--r--vendor/libghostty-vt/src/terminal/c/types.zig3
-rw-r--r--vendor/libghostty-vt/src/terminal/kitty/graphics.zig1
-rw-r--r--vendor/libghostty-vt/src/terminal/kitty/graphics_command.zig114
-rw-r--r--vendor/libghostty-vt/src/terminal/kitty/graphics_exec.zig13
-rw-r--r--vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig271
-rw-r--r--vendor/libghostty-vt/src/terminal/kitty/graphics_storage.zig19
-rw-r--r--vendor/libghostty-vt/src/terminal/main.zig1
-rw-r--r--vendor/libghostty-vt/src/terminal/modes.zig5
-rw-r--r--vendor/libghostty-vt/src/terminal/stream_terminal.zig100
-rw-r--r--vendor/libghostty-vt/src/terminal/sys.zig54
28 files changed, 3735 insertions, 196 deletions
diff --git a/vendor/libghostty-vt/src/terminal/PageList.zig b/vendor/libghostty-vt/src/terminal/PageList.zig
index 39873374..17eea73d 100644
--- a/vendor/libghostty-vt/src/terminal/PageList.zig
+++ b/vendor/libghostty-vt/src/terminal/PageList.zig
@@ -1137,6 +1137,17 @@ fn resizeCols(
for (total..self.rows) |_| _ = try self.grow();
}
+ // Reflow can unwrap enough rows that a history viewport pin lands in the
+ // active area before we do any preserved-cursor growth below. Switch back
+ // to the active viewport now so intermediate grow() integrity checks stay
+ // valid.
+ switch (self.viewport) {
+ .active, .top => {},
+ .pin => if (self.pinIsActive(self.viewport_pin.*)) {
+ self.viewport = .active;
+ },
+ }
+
// See preserved_cursor setup for why.
if (preserved_cursor) |c| cursor: {
const active_pt = self.pointFromPin(
diff --git a/vendor/libghostty-vt/src/terminal/Screen.zig b/vendor/libghostty-vt/src/terminal/Screen.zig
index 77e05b09..39fdd610 100644
--- a/vendor/libghostty-vt/src/terminal/Screen.zig
+++ b/vendor/libghostty-vt/src/terminal/Screen.zig
@@ -255,7 +255,16 @@ pub const Options = struct {
/// The total storage limit for Kitty images in bytes for this
/// screen. Kitty image storage is per-screen.
- kitty_image_storage_limit: usize = 320 * 1000 * 1000, // 320MB
+ kitty_image_storage_limit: usize = switch (build_options.artifact) {
+ .ghostty => 320 * 1000 * 1000, // 320MB
+ .lib => 10 * 1000 * 1000, // 10MB
+ },
+
+ /// The limits for what medium types are allowed for Kitty image loading.
+ kitty_image_loading_limits: if (build_options.kitty_graphics)
+ kitty.graphics.LoadingImage.Limits
+ else
+ void = if (build_options.kitty_graphics) .direct else {},
/// A simple, default terminal. If you rely on specific dimensions or
/// scrollback (or lack of) then do not use this directly. This is just
@@ -313,6 +322,7 @@ pub fn init(
&result,
opts.kitty_image_storage_limit,
) catch unreachable;
+ result.kitty_images.image_limits = opts.kitty_image_loading_limits;
}
return result;
@@ -6487,6 +6497,86 @@ test "Screen: resize more cols with populated scrollback" {
}
}
+test "Screen: resize more cols bounded scrollback keeps viewport valid" {
+ // Regression test for issue #12298.
+ //
+ // This needs to live at the Screen layer rather than PageList because the
+ // bad state only appears once Screen forwards the active cursor into the
+ // resize path. A direct PageList resize repro does not hit the same bug.
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s = try init(alloc, .{
+ .cols = 2,
+ .rows = 10,
+ .max_scrollback = 10_000,
+ });
+ defer s.deinit();
+
+ // Build 30 rows of scrollback on top of our 10-row viewport so we have a
+ // 40-row screen with history above the active area.
+ for (0..30) |_| _ = try s.pages.grow();
+ s.cursorReload();
+ try testing.expectEqual(@as(usize, 40), s.pages.scrollbar().total);
+
+ // Fill the entire screen with two-row wrapped runs:
+ // - even rows mark the end of a wrapped line
+ // - odd rows mark the continuation
+ //
+ // With 2 columns, each logical line occupies two rows. When we grow to 4
+ // columns with reflow enabled, those pairs unwrap back into single rows.
+ // That cuts the total row count down and is what stresses the viewport pin.
+ var it = s.pages.pageIterator(.right_down, .{ .screen = .{} }, null);
+ while (it.next()) |chunk| {
+ const page = &chunk.node.data;
+ for (chunk.start..chunk.end) |y| {
+ const rac = page.getRowAndCell(0, y);
+ if (y % 2 == 0) {
+ rac.row.wrap = true;
+ } else {
+ rac.row.wrap_continuation = true;
+ }
+
+ for (0..s.pages.cols) |x| {
+ page.getRowAndCell(x, y).cell.* = .{
+ .content_tag = .codepoint,
+ .content = .{ .codepoint = 'A' },
+ };
+ }
+ }
+ }
+
+ // Pin the viewport to a history row just above the active area.
+ //
+ // Before resize:
+ // - total rows = 40
+ // - active area starts at row 30
+ // - viewport is pinned at row 28
+ //
+ // After unwrap during resize:
+ // - total rows shrinks to 20
+ // - the old row 28 remaps into what is now the active area
+ //
+ // The bug was that resize/grow would temporarily keep the viewport as a
+ // history pin even after reflow had moved it into the active area, leaving
+ // fewer than `rows` visible rows beneath the pin and tripping integrity
+ // checks.
+ s.pages.scroll(.{ .pin = s.pages.pin(.{ .screen = .{ .y = 28 } }).? });
+ try testing.expect(s.pages.viewport == .pin);
+ try testing.expect(s.pages.getBottomRight(.viewport) != null);
+
+ // Growing columns triggers reflow, which unwraps the synthetic wrapped
+ // rows above. This used to panic during the resize path.
+ try s.resize(.{ .cols = 4, .rows = s.pages.rows, .reflow = true });
+
+ // After the fix, the viewport is normalized back to the active area as
+ // soon as the pinned row lands there, so viewport queries remain valid.
+ try testing.expectEqual(@as(usize, 4), s.pages.cols);
+ try testing.expect(s.pages.scrollbar().total < 40);
+ try testing.expect(s.pages.viewport == .active);
+ try testing.expect(s.pages.getBottomRight(.viewport) != null);
+}
+
test "Screen: resize more cols with reflow" {
const testing = std.testing;
const alloc = testing.allocator;
diff --git a/vendor/libghostty-vt/src/terminal/Terminal.zig b/vendor/libghostty-vt/src/terminal/Terminal.zig
index 99536e7a..6740a93e 100644
--- a/vendor/libghostty-vt/src/terminal/Terminal.zig
+++ b/vendor/libghostty-vt/src/terminal/Terminal.zig
@@ -191,6 +191,26 @@ pub const Options = struct {
/// The default mode state. When the terminal gets a reset, it
/// will revert back to this state.
default_modes: modespkg.ModePacked = .{},
+
+ /// The total storage limit for Kitty images in bytes. Has no effect
+ /// if kitty images are disabled at build-time.
+ kitty_image_storage_limit: usize = switch (build_options.artifact) {
+ .ghostty => 320 * 1000 * 1000, // 320MB
+
+ // libghostty we start with a much lower limit since this is an
+ // embedded library and we want to be more conservative with memory
+ // usage by default.
+ .lib => 10 * 1000 * 1000, // 10MB
+ },
+
+ /// The limits for what medium types are allowed for Kitty image loading.
+ /// Has no effect if kitty images are disabled otherwise. For example,
+ // if no `sys.decode_png` hook is specified, png formats are disabled
+ // no matter what.
+ kitty_image_loading_limits: if (build_options.kitty_graphics)
+ kitty.graphics.LoadingImage.Limits
+ else
+ void = if (build_options.kitty_graphics) .direct else {},
};
/// Initialize a new terminal.
@@ -205,6 +225,8 @@ pub fn init(
.cols = cols,
.rows = rows,
.max_scrollback = opts.max_scrollback,
+ .kitty_image_storage_limit = opts.kitty_image_storage_limit,
+ .kitty_image_loading_limits = opts.kitty_image_loading_limits,
});
errdefer screen_set.deinit(alloc);
@@ -551,20 +573,25 @@ pub fn print(self: *Terminal, c: u21) !void {
// it.
if (self.modes.get(.grapheme_cluster)) return;
- // If we're at cell zero, then this is malformed data and we don't
- // print anything or even store this. Zero-width characters are ALWAYS
- // attached to some other non-zero-width character at the time of
- // writing.
- if (self.screens.active.cursor.x == 0) {
+ // If we have wraparound enabled and a pending wrap, the character
+ // we're attaching to is still under the cursor. Otherwise, it's the
+ // cell to the left.
+ const left: size.CellCountInt = if (self.modes.get(.wraparound) and self.screens.active.cursor.pending_wrap) 0 else 1;
+
+ // If we're at cell zero and not pending a wrap, then this is malformed
+ // data and we don't print anything or even store this. Zero-width
+ // characters are ALWAYS attached to some other non-zero-width
+ // character at the time of writing.
+ if (self.screens.active.cursor.x == 0 and left == 1) {
log.warn("zero-width character with no prior character, ignoring", .{});
return;
}
// Find our previous cell
const prev = prev: {
- const immediate = self.screens.active.cursorCellLeft(1);
+ const immediate = self.screens.active.cursorCellLeft(left);
if (immediate.wide != .spacer_tail) break :prev immediate;
- break :prev self.screens.active.cursorCellLeft(2);
+ break :prev self.screens.active.cursorCellLeft(left + 1);
};
// If our previous cell has no text, just ignore the zero-width character
@@ -2693,6 +2720,34 @@ pub fn kittyGraphics(
return kitty.graphics.execute(alloc, self, cmd);
}
+/// Set the storage size limit for Kitty graphics across all screens.
+pub fn setKittyGraphicsSizeLimit(
+ self: *Terminal,
+ alloc: Allocator,
+ limit: usize,
+) !void {
+ if (comptime !build_options.kitty_graphics) return;
+ var it = self.screens.all.iterator();
+ while (it.next()) |entry| {
+ const screen: *Screen = entry.value.*;
+ try screen.kitty_images.setLimit(alloc, screen, limit);
+ }
+}
+
+/// Set the allowed medium types for Kitty graphics image loading
+/// across all screens.
+pub fn setKittyGraphicsLoadingLimits(
+ self: *Terminal,
+ limits: kitty.graphics.LoadingImage.Limits,
+) void {
+ if (comptime !build_options.kitty_graphics) return;
+ var it = self.screens.all.iterator();
+ while (it.next()) |entry| {
+ const screen: *Screen = entry.value.*;
+ screen.kitty_images.image_limits = limits;
+ }
+}
+
/// Set a style attribute.
pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
try self.screens.active.setAttribute(attr);
@@ -2941,12 +2996,15 @@ pub fn switchScreen(self: *Terminal, key: ScreenSet.Key) !?*Screen {
.alternate => 0,
},
- // Inherit our Kitty image storage limit from the primary
+ // Inherit our Kitty image settings from the primary
// screen if we have to initialize.
.kitty_image_storage_limit = if (comptime build_options.kitty_graphics)
primary.kitty_images.total_limit
else
0,
+ .kitty_image_loading_limits = if (comptime build_options.kitty_graphics)
+ primary.kitty_images.image_limits
+ else {},
},
);
};
@@ -3260,6 +3318,23 @@ test "Terminal: zero-width character at start" {
try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
}
+// https://github.com/ghostty-org/ghostty/issues/12581
+test "Terminal: zero-width character attaches to pending wrap cell" {
+ var t = try init(testing.allocator, .{ .cols = 2, .rows = 2 });
+ defer t.deinit(testing.allocator);
+
+ // Disable grapheme clustering to exercise the fallback path.
+ t.modes.set(.grapheme_cluster, false);
+
+ try t.print('x');
+ try t.print('å');
+ try t.print(0x0332); // Combining low line.
+
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("xå̲", str);
+}
+
// https://github.com/mitchellh/ghostty/issues/1400
test "Terminal: print single very long line" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
diff --git a/vendor/libghostty-vt/src/terminal/apc.zig b/vendor/libghostty-vt/src/terminal/apc.zig
index 3ebacbbf..7e6f08a7 100644
--- a/vendor/libghostty-vt/src/terminal/apc.zig
+++ b/vendor/libghostty-vt/src/terminal/apc.zig
@@ -10,7 +10,15 @@ const log = std.log.scoped(.terminal_apc);
/// The start/feed/end functions are meant to be called from the terminal.Stream
/// apcStart, apcPut, and apcEnd functions, respectively.
pub const Handler = struct {
- state: State = .{ .inactive = {} },
+ state: State = .inactive,
+
+ /// Maximum bytes each APC protocol can buffer. This is to prevent
+ /// malicious input from causing us to allocate too much memory.
+ /// If you want to be lazy and set a single value for all protocols,
+ /// use `.initFull`.
+ max_bytes: std.EnumMap(Protocol, usize) = .initFullWith(.{
+ .kitty = Protocol.defaultMaxBytes(.kitty),
+ }),
pub fn deinit(self: *Handler) void {
self.state.deinit();
@@ -34,19 +42,24 @@ pub const Handler = struct {
switch (byte) {
// Kitty graphics protocol
'G' => self.state = if (comptime build_options.kitty_graphics)
- .{ .kitty = kitty_gfx.CommandParser.init(alloc) }
+ .{ .kitty = .init(
+ alloc,
+ self.max_bytes.get(.kitty) orelse
+ Protocol.defaultMaxBytes(.kitty),
+ ) }
else
- .{ .ignore = {} },
+ .ignore,
// Unknown
- else => self.state = .{ .ignore = {} },
+ else => self.state = .ignore,
}
},
.kitty => |*p| if (comptime build_options.kitty_graphics) {
p.feed(byte) catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
- self.state = .{ .ignore = {} };
+ p.deinit();
+ self.state = .ignore;
};
} else unreachable,
}
@@ -55,7 +68,7 @@ pub const Handler = struct {
pub fn end(self: *Handler) ?Command {
defer {
self.state.deinit();
- self.state = .{ .inactive = {} };
+ self.state = .inactive;
}
return switch (self.state) {
@@ -106,8 +119,22 @@ pub const State = union(enum) {
}
};
+/// Possible APC command types.
+pub const Protocol = enum {
+ kitty,
+
+ /// Returns the default maximum bytes for the given protocol.
+ pub fn defaultMaxBytes(self: Protocol) usize {
+ return switch (self) {
+ // Kitty graphics payloads can be very large (e.g. full images
+ // encoded as base64), so the default is set to 65 MiB.
+ .kitty => 65 * 1024 * 1024,
+ };
+ }
+};
+
/// Possible APC commands.
-pub const Command = union(enum) {
+pub const Command = union(Protocol) {
kitty: if (build_options.kitty_graphics)
kitty_gfx.Command
else
@@ -169,6 +196,41 @@ test "Kitty command with overflow i32" {
try testing.expect(h.end() == null);
}
+test "kitty feed error deinits parser" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Feed a valid kitty command start to allocate parser state, then
+ // trigger an error during feed via an integer overflow. The testing
+ // allocator will detect leaks if deinit is not called.
+ var h: Handler = .{};
+ defer h.deinit();
+ h.start();
+ for ("Ga=p,i=10000000000;") |c| h.feed(alloc, c);
+ try testing.expect(h.state == .ignore);
+}
+
+test "kitty max bytes exceeded" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{ .max_bytes = .init(.{ .kitty = 4 }) };
+ defer h.deinit();
+ h.start();
+ // 'G' identifies kitty, 'a=t;' moves to data state, then feed exceeds max_bytes.
+ for ("Ga=t;") |c| h.feed(alloc, c);
+ try testing.expect(h.state != .ignore);
+ for ("abcd") |c| h.feed(alloc, c);
+ try testing.expect(h.state != .ignore);
+ // The 5th data byte exceeds the 4-byte limit.
+ h.feed(alloc, 'e');
+ try testing.expect(h.state == .ignore);
+}
+
test "valid Kitty command" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
diff --git a/vendor/libghostty-vt/src/terminal/build_options.zig b/vendor/libghostty-vt/src/terminal/build_options.zig
index 6c0a4df6..136e0f10 100644
--- a/vendor/libghostty-vt/src/terminal/build_options.zig
+++ b/vendor/libghostty-vt/src/terminal/build_options.zig
@@ -2,6 +2,14 @@ const std = @import("std");
/// Options set by Zig build.zig and exposed via `terminal_options`.
pub const Options = struct {
+ pub const Artifact = enum {
+ /// Ghostty application
+ ghostty,
+
+ /// libghostty-vt, Zig module
+ lib,
+ };
+
/// The target artifact to build. This will gate some functionality.
artifact: Artifact,
@@ -47,34 +55,40 @@ pub const Options = struct {
opts.addOption(bool, "simd", self.simd);
opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety);
+ // Kitty graphics is almost always true. This used to be conditional on
+ // some other factors but we've since generalized the implementation
+ // to support optional PNG decoding, OS capabilities like filesystems,
+ // etc. So its safe to always enable it and just have the
+ // implementation deal with unsupported features as needed.
+ //
+ // We disable it on wasm32-freestanding because we at the least
+ // require the ability to get timestamps and there is no way to
+ // do that with freestanding targets.
+ const target = m.resolved_target.?.result;
+ opts.addOption(
+ bool,
+ "kitty_graphics",
+ !(target.cpu.arch == .wasm32 and target.os.tag == .freestanding),
+ );
+
// These are synthesized based on other options.
- opts.addOption(bool, "kitty_graphics", self.oniguruma);
opts.addOption(bool, "tmux_control_mode", self.oniguruma);
// Version information.
- var buf: [1024]u8 = undefined;
opts.addOption(
[]const u8,
"version_string",
- std.fmt.bufPrint(
- &buf,
+ b.fmt(
"{f}",
.{self.version},
- ) catch @panic("version string too long"),
+ ),
);
opts.addOption(usize, "version_major", self.version.major);
opts.addOption(usize, "version_minor", self.version.minor);
opts.addOption(usize, "version_patch", self.version.patch);
+ opts.addOption(?[]const u8, "version_pre", self.version.pre);
opts.addOption(?[]const u8, "version_build", self.version.build);
m.addOptions("terminal_options", opts);
}
};
-
-pub const Artifact = enum {
- /// Ghostty application
- ghostty,
-
- /// libghostty-vt, Zig module
- lib,
-};
diff --git a/vendor/libghostty-vt/src/terminal/c/AGENTS.md b/vendor/libghostty-vt/src/terminal/c/AGENTS.md
index 63f7fc6c..c7e9068a 100644
--- a/vendor/libghostty-vt/src/terminal/c/AGENTS.md
+++ b/vendor/libghostty-vt/src/terminal/c/AGENTS.md
@@ -5,7 +5,12 @@
via `lib.TaggedUnion`.
- Any functions must be updated all the way through from here to
`src/terminal/c/main.zig` to `src/lib_vt.zig` and the headers
- in `include/ghostty/vt.h`.
+ in `include/ghostty/vt.h`. Specifically:
+ 1. Define the function in `src/terminal/c/<module>.zig`.
+ 2. Re-export it via a `pub const` in `src/terminal/c/main.zig`.
+ 3. Add an `@export` call in `src/lib_vt.zig` with the
+ `ghostty_` prefixed symbol name.
+ 4. Declare it in the corresponding header under `include/ghostty/vt/`.
- In `include/ghostty/vt.h`, always sort the header contents by:
(1) macros, (2) forward declarations, (3) types, (4) functions
diff --git a/vendor/libghostty-vt/src/terminal/c/build_info.zig b/vendor/libghostty-vt/src/terminal/c/build_info.zig
index bef6760f..8e0cd4d6 100644
--- a/vendor/libghostty-vt/src/terminal/c/build_info.zig
+++ b/vendor/libghostty-vt/src/terminal/c/build_info.zig
@@ -25,7 +25,8 @@ pub const BuildInfo = enum(c_int) {
version_major = 6,
version_minor = 7,
version_patch = 8,
- version_build = 9,
+ version_pre = 9,
+ version_build = 10,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: BuildInfo) type {
@@ -33,7 +34,7 @@ pub const BuildInfo = enum(c_int) {
.invalid => void,
.simd, .kitty_graphics, .tmux_control_mode => bool,
.optimize => OptimizeMode,
- .version_string, .version_build => lib.String,
+ .version_string, .version_pre, .version_build => lib.String,
.version_major, .version_minor, .version_patch => usize,
};
}
@@ -78,6 +79,13 @@ fn getTyped(
.version_major => out.* = build_options.version_major,
.version_minor => out.* = build_options.version_minor,
.version_patch => out.* = build_options.version_patch,
+ .version_pre => {
+ if (build_options.version_pre) |b| {
+ out.* = .{ .ptr = b.ptr, .len = b.len };
+ } else {
+ out.* = .{ .ptr = "", .len = 0 };
+ }
+ },
.version_build => {
if (build_options.version_build) |b| {
out.* = .{ .ptr = b.ptr, .len = b.len };
@@ -151,6 +159,12 @@ test "get version_patch" {
try testing.expectEqual(build_options.version_patch, value);
}
+test "get version_pre" {
+ const testing = std.testing;
+ var value: lib.String = undefined;
+ try testing.expectEqual(Result.success, get(.version_pre, @ptrCast(&value)));
+}
+
test "get version_build" {
const testing = std.testing;
var value: lib.String = undefined;
diff --git a/vendor/libghostty-vt/src/terminal/c/cell.zig b/vendor/libghostty-vt/src/terminal/c/cell.zig
index c9746da7..b59567df 100644
--- a/vendor/libghostty-vt/src/terminal/c/cell.zig
+++ b/vendor/libghostty-vt/src/terminal/c/cell.zig
@@ -120,6 +120,27 @@ pub fn get(
};
}
+pub fn get_multi(
+ cell_: CCell,
+ count: usize,
+ keys: ?[*]const CellData,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = get(cell_, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
fn getTyped(
cell_: CCell,
comptime data: CellData,
@@ -176,3 +197,42 @@ test "get wide" {
try testing.expectEqual(Result.success, get(cell, .wide, @ptrCast(&w)));
try testing.expectEqual(Wide.wide, w);
}
+
+test "get_multi success" {
+ const cell: CCell = @bitCast(Cell.init('B'));
+ var cp: u32 = 0;
+ var has_text: bool = false;
+ var written: usize = 0;
+
+ const keys = [_]CellData{ .codepoint, .has_text };
+ var values = [_]?*anyopaque{ @ptrCast(&cp), @ptrCast(&has_text) };
+ try testing.expectEqual(Result.success, get_multi(cell, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+ try testing.expectEqual(@as(u32, 'B'), cp);
+ try testing.expect(has_text);
+}
+
+test "get_multi error sets out_written" {
+ const cell: CCell = @bitCast(Cell.init('C'));
+ var cp: u32 = 0;
+ var written: usize = 99;
+
+ const keys = [_]CellData{ .codepoint, .invalid };
+ var values = [_]?*anyopaque{ @ptrCast(&cp), @ptrCast(&cp) };
+ try testing.expectEqual(Result.invalid_value, get_multi(cell, keys.len, &keys, &values, &written));
+ try testing.expectEqual(1, written);
+ try testing.expectEqual(@as(u32, 'C'), cp);
+}
+
+test "get_multi null keys returns invalid_value" {
+ const cell: CCell = @bitCast(Cell.init('A'));
+ var cp: u32 = 0;
+ var values = [_]?*anyopaque{@ptrCast(&cp)};
+ try testing.expectEqual(Result.invalid_value, get_multi(cell, 1, null, &values, null));
+}
+
+test "get_multi null values returns invalid_value" {
+ const cell: CCell = @bitCast(Cell.init('A'));
+ const keys = [_]CellData{.codepoint};
+ try testing.expectEqual(Result.invalid_value, get_multi(cell, 1, &keys, null, null));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/formatter.zig b/vendor/libghostty-vt/src/terminal/c/formatter.zig
index 11717bc2..5b4504c8 100644
--- a/vendor/libghostty-vt/src/terminal/c/formatter.zig
+++ b/vendor/libghostty-vt/src/terminal/c/formatter.zig
@@ -3,6 +3,8 @@ const testing = std.testing;
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const terminal_c = @import("terminal.zig");
+const grid_ref = @import("grid_ref.zig");
+const selection_c = @import("selection.zig");
const ZigTerminal = @import("../Terminal.zig");
const formatterpkg = @import("../formatter.zig");
const Result = @import("result.zig").Result;
@@ -23,6 +25,8 @@ pub const Formatter = ?*FormatterWrapper;
/// C: GhosttyFormatterFormat
pub const Format = formatterpkg.Format;
+const CSelection = selection_c.CSelection;
+
/// C: GhosttyFormatterScreenOptions
pub const ScreenOptions = extern struct {
/// C: GhosttyFormatterScreenExtra
@@ -63,6 +67,10 @@ pub const TerminalOptions = extern struct {
trim: bool,
extra: Extra,
+ /// Optional selection to restrict output to a range.
+ /// If null, the entire screen is formatted.
+ selection: ?*const CSelection = null,
+
/// C: GhosttyFormatterTerminalExtra
pub const Extra = extern struct {
size: usize = @sizeOf(Extra),
@@ -138,6 +146,12 @@ fn terminal_new_(
});
formatter.extra = opts.extra.toZig();
+ // Setup the content that we're formatting
+ if (opts.selection) |sel| formatter.content = .{
+ .selection = sel.toZig() orelse
+ return error.InvalidValue,
+ };
+
ptr.* = .{
.kind = .{ .terminal = formatter },
.alloc = alloc,
@@ -389,6 +403,50 @@ test "format vt" {
try testing.expect(std.mem.indexOf(u8, buf[0..written], "Test") != null);
}
+test "format plain with 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);
+
+ // Get grid refs for "World" (columns 6..10 on row 0)
+ 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: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ };
+
+ var f: Formatter = null;
+ try testing.expectEqual(Result.success, terminal_new(
+ &lib.alloc.test_allocator,
+ &f,
+ t,
+ .{ .emit = .plain, .unwrap = false, .trim = true, .selection = &sel, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
+ ));
+ defer free(f);
+
+ var buf: [1024]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
+ try testing.expectEqualStrings("World", buf[0..written]);
+}
+
test "format html" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
diff --git a/vendor/libghostty-vt/src/terminal/c/grid_ref.zig b/vendor/libghostty-vt/src/terminal/c/grid_ref.zig
index d029c595..db7ff2e8 100644
--- a/vendor/libghostty-vt/src/terminal/c/grid_ref.zig
+++ b/vendor/libghostty-vt/src/terminal/c/grid_ref.zig
@@ -3,11 +3,13 @@ const testing = std.testing;
const lib = @import("../lib.zig");
const page = @import("../page.zig");
const PageList = @import("../PageList.zig");
+const point = @import("../point.zig");
const size = @import("../size.zig");
const stylepkg = @import("../style.zig");
const cell_c = @import("cell.zig");
const row_c = @import("row.zig");
const style_c = @import("style.zig");
+const terminal_c = @import("terminal.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyGridRef
@@ -29,7 +31,7 @@ pub const CGridRef = extern struct {
};
}
- fn toPin(self: CGridRef) ?PageList.Pin {
+ pub fn toPin(self: CGridRef) ?PageList.Pin {
return .{
.node = self.node orelse return null,
.x = self.x,
@@ -89,6 +91,38 @@ pub fn grid_ref_graphemes(
return .success;
}
+pub fn grid_ref_hyperlink_uri(
+ ref: *const CGridRef,
+ out_buf: ?[*]u8,
+ buf_len: usize,
+ out_len: *usize,
+) callconv(lib.calling_conv) Result {
+ const p = ref.toPin() orelse return .invalid_value;
+ const rac = p.node.data.getRowAndCell(p.x, p.y);
+ const cell = rac.cell;
+
+ if (!cell.hyperlink) {
+ out_len.* = 0;
+ return .success;
+ }
+
+ const link_id = p.node.data.lookupHyperlink(cell) orelse {
+ out_len.* = 0;
+ return .success;
+ };
+ const entry = p.node.data.hyperlink_set.get(p.node.data.memory, link_id);
+ const uri = entry.uri.slice(p.node.data.memory);
+
+ if (out_buf == null or buf_len < uri.len) {
+ out_len.* = uri.len;
+ return .out_of_space;
+ }
+
+ @memcpy(out_buf.?[0..uri.len], uri);
+ out_len.* = uri.len;
+ return .success;
+}
+
pub fn grid_ref_style(
ref: *const CGridRef,
out: ?*style_c.Style,
@@ -154,3 +188,63 @@ test "grid_ref_style null out" {
const ref = CGridRef{};
try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, null));
}
+
+test "grid_ref_hyperlink_uri null node" {
+ const ref = CGridRef{};
+ var len: usize = undefined;
+ try testing.expectEqual(Result.invalid_value, grid_ref_hyperlink_uri(&ref, null, 0, &len));
+}
+
+test "grid_ref_hyperlink_uri no hyperlink" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(terminal);
+
+ terminal_c.vt_write(terminal, "hello", 5);
+
+ var ref: CGridRef = undefined;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(
+ terminal,
+ point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
+ &ref,
+ ));
+
+ var len: usize = undefined;
+ try testing.expectEqual(Result.success, grid_ref_hyperlink_uri(&ref, null, 0, &len));
+ try testing.expectEqual(@as(usize, 0), len);
+}
+
+test "grid_ref_hyperlink_uri with hyperlink" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(terminal);
+
+ // Write OSC 8 hyperlink: \e]8;;uri\e\\text\e]8;;\e\\
+ const seq = "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\";
+ terminal_c.vt_write(terminal, seq, seq.len);
+
+ var ref: CGridRef = undefined;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(
+ terminal,
+ point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
+ &ref,
+ ));
+
+ // First query length with null buf
+ var len: usize = undefined;
+ try testing.expectEqual(Result.out_of_space, grid_ref_hyperlink_uri(&ref, null, 0, &len));
+ try testing.expectEqual(@as(usize, 19), len); // "https://example.com"
+
+ // Now read with a properly sized buffer
+ var buf: [256]u8 = undefined;
+ try testing.expectEqual(Result.success, grid_ref_hyperlink_uri(&ref, &buf, buf.len, &len));
+ try testing.expectEqualStrings("https://example.com", buf[0..len]);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/key_encode.zig b/vendor/libghostty-vt/src/terminal/c/key_encode.zig
index 15fa74dd..f5d459f0 100644
--- a/vendor/libghostty-vt/src/terminal/c/key_encode.zig
+++ b/vendor/libghostty-vt/src/terminal/c/key_encode.zig
@@ -52,6 +52,11 @@ pub const Option = enum(c_int) {
modify_other_keys_state_2 = 4,
kitty_flags = 5,
macos_option_as_alt = 6,
+ /// DEC Backarrow Key Mode (DECBKM)
+ /// See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170
+ /// If `false` (the default), `backspace` emits 0x7f
+ /// If `true`, `backspace` emits 0x08
+ backarrow_key_mode = 7,
/// Input type expected for setting the option.
pub fn InType(comptime self: Option) type {
@@ -61,6 +66,7 @@ pub const Option = enum(c_int) {
.ignore_keypad_with_numlock,
.alt_esc_prefix,
.modify_other_keys_state_2,
+ .backarrow_key_mode,
=> bool,
.kitty_flags => u8,
.macos_option_as_alt => OptionAsAlt,
@@ -114,6 +120,7 @@ fn setoptTyped(
}
opts.macos_option_as_alt = value.*;
},
+ .backarrow_key_mode => opts.backarrow_key_mode = value.*,
}
}
diff --git a/vendor/libghostty-vt/src/terminal/c/kitty_graphics.zig b/vendor/libghostty-vt/src/terminal/c/kitty_graphics.zig
new file mode 100644
index 00000000..d50b8c4c
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/kitty_graphics.zig
@@ -0,0 +1,1710 @@
+const std = @import("std");
+const testing = std.testing;
+const build_options = @import("terminal_options");
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const kitty_storage = @import("../kitty/graphics_storage.zig");
+const kitty_cmd = @import("../kitty/graphics_command.zig");
+const Image = @import("../kitty/graphics_image.zig").Image;
+const grid_ref = @import("grid_ref.zig");
+const selection_c = @import("selection.zig");
+const terminal_c = @import("terminal.zig");
+const Terminal = @import("../Terminal.zig");
+const Result = @import("result.zig").Result;
+
+/// C: GhosttyKittyGraphics
+pub const KittyGraphics = if (build_options.kitty_graphics)
+ *kitty_storage.ImageStorage
+else
+ *anyopaque;
+
+/// C: GhosttyKittyGraphicsImage
+pub const ImageHandle = if (build_options.kitty_graphics)
+ ?*const Image
+else
+ ?*const anyopaque;
+
+/// C: GhosttyKittyGraphicsPlacementIterator
+pub const PlacementIterator = if (build_options.kitty_graphics)
+ ?*PlacementIteratorWrapper
+else
+ ?*anyopaque;
+
+const PlacementMap = if (build_options.kitty_graphics)
+ std.AutoHashMapUnmanaged(
+ kitty_storage.ImageStorage.PlacementKey,
+ kitty_storage.ImageStorage.Placement,
+ )
+else
+ void;
+
+const PlacementIteratorWrapper = if (build_options.kitty_graphics)
+ struct {
+ alloc: std.mem.Allocator,
+ inner: PlacementMap.Iterator = undefined,
+ entry: ?PlacementMap.Entry = null,
+ layer_filter: PlacementLayer = .all,
+ }
+else
+ void;
+
+/// C: GhosttyKittyGraphicsData
+pub const Data = enum(c_int) {
+ invalid = 0,
+ placement_iterator = 1,
+
+ pub fn OutType(comptime self: Data) type {
+ return switch (self) {
+ .invalid => void,
+ .placement_iterator => PlacementIterator,
+ };
+ }
+};
+
+/// C: GhosttyKittyGraphicsPlacementData
+pub const PlacementData = enum(c_int) {
+ invalid = 0,
+ image_id = 1,
+ placement_id = 2,
+ is_virtual = 3,
+ x_offset = 4,
+ y_offset = 5,
+ source_x = 6,
+ source_y = 7,
+ source_width = 8,
+ source_height = 9,
+ columns = 10,
+ rows = 11,
+ z = 12,
+
+ pub fn OutType(comptime self: PlacementData) type {
+ return switch (self) {
+ .invalid => void,
+ .image_id, .placement_id => u32,
+ .is_virtual => bool,
+ .x_offset,
+ .y_offset,
+ .source_x,
+ .source_y,
+ .source_width,
+ .source_height,
+ .columns,
+ .rows,
+ => u32,
+ .z => i32,
+ };
+ }
+};
+
+pub fn get(
+ graphics_: KittyGraphics,
+ data: Data,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| getTyped(
+ graphics_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+fn getTyped(
+ graphics_: KittyGraphics,
+ comptime data: Data,
+ out: *data.OutType(),
+) Result {
+ const storage = graphics_;
+ switch (data) {
+ .invalid => return .invalid_value,
+ .placement_iterator => {
+ const it = out.* orelse return .invalid_value;
+ it.* = .{
+ .alloc = it.alloc,
+ .inner = storage.placements.iterator(),
+ .layer_filter = it.layer_filter,
+ };
+ },
+ }
+ return .success;
+}
+
+/// C: GhosttyKittyPlacementLayer
+pub const PlacementLayer = enum(c_int) {
+ all = 0,
+ below_bg = 1,
+ below_text = 2,
+ above_text = 3,
+
+ fn matches(self: PlacementLayer, z: i32) bool {
+ return switch (self) {
+ .all => true,
+ .below_bg => z < std.math.minInt(i32) / 2,
+ .below_text => z >= std.math.minInt(i32) / 2 and z < 0,
+ .above_text => z >= 0,
+ };
+ }
+};
+
+/// C: GhosttyKittyGraphicsPlacementIteratorOption
+pub const PlacementIteratorOption = enum(c_int) {
+ layer = 0,
+
+ pub fn InType(comptime self: PlacementIteratorOption) type {
+ return switch (self) {
+ .layer => PlacementLayer,
+ };
+ }
+};
+
+/// C: GhosttyKittyImageFormat
+pub const ImageFormat = kitty_cmd.Transmission.Format;
+
+/// C: GhosttyKittyImageCompression
+pub const ImageCompression = kitty_cmd.Transmission.Compression;
+
+/// C: GhosttyKittyGraphicsImageData
+pub const ImageData = enum(c_int) {
+ invalid = 0,
+ id = 1,
+ number = 2,
+ width = 3,
+ height = 4,
+ format = 5,
+ compression = 6,
+ data_ptr = 7,
+ data_len = 8,
+
+ pub fn OutType(comptime self: ImageData) type {
+ return switch (self) {
+ .invalid => void,
+ .id, .number, .width, .height => u32,
+ .format => ImageFormat,
+ .compression => ImageCompression,
+ .data_ptr => [*]const u8,
+ .data_len => usize,
+ };
+ }
+};
+
+pub fn image_get_handle(
+ graphics_: KittyGraphics,
+ image_id: u32,
+) callconv(lib.calling_conv) ImageHandle {
+ if (comptime !build_options.kitty_graphics) return null;
+
+ const storage = graphics_;
+ return storage.images.getPtr(image_id);
+}
+
+pub fn image_get(
+ image_: ImageHandle,
+ data: ImageData,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| imageGetTyped(
+ image_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+pub fn image_get_multi(
+ image_: ImageHandle,
+ count: usize,
+ keys: ?[*]const ImageData,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = image_get(image_, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
+fn imageGetTyped(
+ image_: ImageHandle,
+ comptime data: ImageData,
+ out: *data.OutType(),
+) Result {
+ const image = image_ orelse return .invalid_value;
+
+ switch (data) {
+ .invalid => return .invalid_value,
+ .id => out.* = image.id,
+ .number => out.* = image.number,
+ .width => out.* = image.width,
+ .height => out.* = image.height,
+ .format => out.* = image.format,
+ .compression => out.* = image.compression,
+ .data_ptr => out.* = image.data.ptr,
+ .data_len => out.* = image.data.len,
+ }
+
+ return .success;
+}
+
+pub fn placement_iterator_new(
+ alloc_: ?*const CAllocator,
+ out: *PlacementIterator,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) {
+ out.* = null;
+ return .no_value;
+ }
+ const alloc = lib.alloc.default(alloc_);
+ const ptr = alloc.create(PlacementIteratorWrapper) catch {
+ out.* = null;
+ return .out_of_memory;
+ };
+ ptr.* = .{ .alloc = alloc };
+ out.* = ptr;
+ return .success;
+}
+
+pub fn placement_iterator_free(iter_: PlacementIterator) callconv(lib.calling_conv) void {
+ if (comptime !build_options.kitty_graphics) return;
+ const iter = iter_ orelse return;
+ iter.alloc.destroy(iter);
+}
+
+pub fn placement_iterator_set(
+ iter_: PlacementIterator,
+ option: PlacementIteratorOption,
+ value: ?*const anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(PlacementIteratorOption, @intFromEnum(option)) catch {
+ return .invalid_value;
+ };
+ }
+
+ return switch (option) {
+ inline else => |comptime_option| placementIteratorSetTyped(
+ iter_,
+ comptime_option,
+ @ptrCast(@alignCast(value orelse return .invalid_value)),
+ ),
+ };
+}
+
+fn placementIteratorSetTyped(
+ iter_: PlacementIterator,
+ comptime option: PlacementIteratorOption,
+ value: *const option.InType(),
+) Result {
+ const iter = iter_ orelse return .invalid_value;
+ switch (option) {
+ .layer => iter.layer_filter = value.*,
+ }
+ return .success;
+}
+
+pub fn placement_iterator_next(iter_: PlacementIterator) callconv(lib.calling_conv) bool {
+ if (comptime !build_options.kitty_graphics) return false;
+
+ const iter = iter_ orelse return false;
+ while (iter.inner.next()) |entry| {
+ if (iter.layer_filter.matches(entry.value_ptr.z)) {
+ iter.entry = entry;
+ return true;
+ }
+ }
+ return false;
+}
+
+pub fn placement_get(
+ iter_: PlacementIterator,
+ data: PlacementData,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ return switch (data) {
+ .invalid => .invalid_value,
+ inline else => |comptime_data| placementGetTyped(
+ iter_,
+ comptime_data,
+ @ptrCast(@alignCast(out)),
+ ),
+ };
+}
+
+pub fn placement_get_multi(
+ iter_: PlacementIterator,
+ count: usize,
+ keys: ?[*]const PlacementData,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = placement_get(iter_, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
+fn placementGetTyped(
+ iter_: PlacementIterator,
+ comptime data: PlacementData,
+ out: *data.OutType(),
+) Result {
+ const iter = iter_ orelse return .invalid_value;
+ const entry = iter.entry orelse return .invalid_value;
+
+ const key = entry.key_ptr;
+ const val = entry.value_ptr;
+
+ switch (data) {
+ .invalid => return .invalid_value,
+ .image_id => out.* = key.image_id,
+ .placement_id => out.* = key.placement_id.id,
+ .is_virtual => out.* = val.location == .virtual,
+ .x_offset => out.* = val.x_offset,
+ .y_offset => out.* = val.y_offset,
+ .source_x => out.* = val.source_x,
+ .source_y => out.* = val.source_y,
+ .source_width => out.* = val.source_width,
+ .source_height => out.* = val.source_height,
+ .columns => out.* = val.columns,
+ .rows => out.* = val.rows,
+ .z => out.* = val.z,
+ }
+
+ return .success;
+}
+
+pub fn placement_rect(
+ iter_: PlacementIterator,
+ image_: ImageHandle,
+ terminal_: terminal_c.Terminal,
+ out: *selection_c.CSelection,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ const wrapper = terminal_ orelse return .invalid_value;
+ const image = image_ orelse return .invalid_value;
+ const iter = iter_ orelse return .invalid_value;
+ const entry = iter.entry orelse return .invalid_value;
+ const r = entry.value_ptr.rect(
+ image.*,
+ wrapper.terminal,
+ ) orelse return .no_value;
+
+ out.* = .{
+ .start = grid_ref.CGridRef.fromPin(r.top_left),
+ .end = grid_ref.CGridRef.fromPin(r.bottom_right),
+ .rectangle = true,
+ };
+
+ return .success;
+}
+
+pub fn placement_pixel_size(
+ iter_: PlacementIterator,
+ image_: ImageHandle,
+ terminal_: terminal_c.Terminal,
+ out_width: *u32,
+ out_height: *u32,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ const wrapper = terminal_ orelse return .invalid_value;
+ const image = image_ orelse return .invalid_value;
+ const iter = iter_ orelse return .invalid_value;
+ const entry = iter.entry orelse return .invalid_value;
+ const s = entry.value_ptr.pixelSize(image.*, wrapper.terminal);
+
+ out_width.* = s.width;
+ out_height.* = s.height;
+
+ return .success;
+}
+
+pub fn placement_grid_size(
+ iter_: PlacementIterator,
+ image_: ImageHandle,
+ terminal_: terminal_c.Terminal,
+ out_cols: *u32,
+ out_rows: *u32,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ const wrapper = terminal_ orelse return .invalid_value;
+ const image = image_ orelse return .invalid_value;
+ const iter = iter_ orelse return .invalid_value;
+ const entry = iter.entry orelse return .invalid_value;
+ const s = entry.value_ptr.gridSize(image.*, wrapper.terminal);
+
+ out_cols.* = s.cols;
+ out_rows.* = s.rows;
+
+ return .success;
+}
+
+pub fn placement_viewport_pos(
+ iter_: PlacementIterator,
+ image_: ImageHandle,
+ terminal_: terminal_c.Terminal,
+ out_col: *i32,
+ out_row: *i32,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ const wrapper = terminal_ orelse return .invalid_value;
+ const image = image_ orelse return .invalid_value;
+ const iter = iter_ orelse return .invalid_value;
+ const entry = iter.entry orelse return .invalid_value;
+
+ const vp = computeViewportPos(entry.value_ptr, image, wrapper.terminal);
+ if (!vp.visible) return .no_value;
+
+ out_col.* = vp.col;
+ out_row.* = vp.row;
+
+ return .success;
+}
+
+pub fn placement_source_rect(
+ iter_: PlacementIterator,
+ image_: ImageHandle,
+ out_x: *u32,
+ out_y: *u32,
+ out_width: *u32,
+ out_height: *u32,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ const image = image_ orelse return .invalid_value;
+ const iter = iter_ orelse return .invalid_value;
+ const entry = iter.entry orelse return .invalid_value;
+ const p = entry.value_ptr;
+
+ // Apply "0 = full image dimension" convention, then clamp to image bounds.
+ const x = @min(p.source_x, image.width);
+ const y = @min(p.source_y, image.height);
+ const w = @min(if (p.source_width > 0) p.source_width else image.width, image.width - x);
+ const h = @min(if (p.source_height > 0) p.source_height else image.height, image.height - y);
+
+ out_x.* = x;
+ out_y.* = y;
+ out_width.* = w;
+ out_height.* = h;
+
+ return .success;
+}
+
+/// C: GhosttyKittyGraphicsPlacementRenderInfo
+pub const PlacementRenderInfo = extern struct {
+ size: usize = @sizeOf(PlacementRenderInfo),
+ pixel_width: u32 = 0,
+ pixel_height: u32 = 0,
+ grid_cols: u32 = 0,
+ grid_rows: u32 = 0,
+ viewport_col: i32 = 0,
+ viewport_row: i32 = 0,
+ viewport_visible: bool = false,
+ source_x: u32 = 0,
+ source_y: u32 = 0,
+ source_width: u32 = 0,
+ source_height: u32 = 0,
+};
+
+pub fn placement_render_info(
+ iter_: PlacementIterator,
+ image_: ImageHandle,
+ terminal_: terminal_c.Terminal,
+ out_: ?*PlacementRenderInfo,
+) callconv(lib.calling_conv) Result {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+
+ const wrapper = terminal_ orelse return .invalid_value;
+ const image = image_ orelse return .invalid_value;
+ const iter = iter_ orelse return .invalid_value;
+ const entry = iter.entry orelse return .invalid_value;
+ const out = out_ orelse return .invalid_value;
+ if (out.size < @sizeOf(PlacementRenderInfo)) return .invalid_value;
+
+ const p = entry.value_ptr;
+
+ const ps = p.pixelSize(image.*, wrapper.terminal);
+ out.pixel_width = ps.width;
+ out.pixel_height = ps.height;
+
+ const gs = p.gridSize(image.*, wrapper.terminal);
+ out.grid_cols = gs.cols;
+ out.grid_rows = gs.rows;
+
+ const vp = computeViewportPos(p, image, wrapper.terminal);
+ out.viewport_col = vp.col;
+ out.viewport_row = vp.row;
+ out.viewport_visible = vp.visible;
+
+ const x = @min(p.source_x, image.width);
+ const y = @min(p.source_y, image.height);
+ out.source_x = x;
+ out.source_y = y;
+ out.source_width = @min(if (p.source_width > 0) p.source_width else image.width, image.width - x);
+ out.source_height = @min(if (p.source_height > 0) p.source_height else image.height, image.height - y);
+
+ return .success;
+}
+
+/// Compute viewport-relative position of a placement.
+///
+/// Converts the placement's internal pin to viewport-relative column
+/// and row coordinates by getting screen-absolute coordinates for
+/// both the pin and the viewport origin, then subtracting to get
+/// viewport-relative coordinates. The row value can be negative when
+/// the placement's origin has scrolled above the top of the viewport.
+///
+/// A placement is considered not visible if it is a virtual (unicode
+/// placeholder) placement, or if it is fully off-screen (its bottom
+/// edge is above the viewport or its top edge is at or below the
+/// viewport's last row).
+fn computeViewportPos(
+ p: *const kitty_storage.ImageStorage.Placement,
+ image: *const Image,
+ t: *Terminal,
+) struct { col: i32, row: i32, visible: bool } {
+ // Virtual placements use unicode placeholders and don't have a
+ // screen position — they are rendered inline by the text layout.
+ const pin = switch (p.location) {
+ .pin => |pin| pin,
+ .virtual => return .{ .col = 0, .row = 0, .visible = false },
+ };
+
+ // Convert both the placement's pin and the viewport's top-left
+ // corner to screen-absolute coordinates so we can subtract them
+ // to get viewport-relative coordinates.
+ const pages = &t.screens.active.pages;
+ const pin_screen = pages.pointFromPin(.screen, pin.*) orelse
+ return .{ .col = 0, .row = 0, .visible = false };
+ const vp_tl = pages.getTopLeft(.viewport);
+ const vp_screen = pages.pointFromPin(.screen, vp_tl) orelse
+ return .{ .col = 0, .row = 0, .visible = false };
+
+ // Subtracting viewport origin from the pin gives us viewport-
+ // relative coordinates. The row can be negative when the
+ // placement has partially scrolled above the viewport.
+ const vp_row: i32 = @as(i32, @intCast(pin_screen.screen.y)) -
+ @as(i32, @intCast(vp_screen.screen.y));
+ const vp_col: i32 = @intCast(pin_screen.screen.x);
+
+ // A placement is invisible if its bottom edge (row + height)
+ // is above the viewport, or its top edge is at or below the
+ // viewport's last row.
+ const grid_size = p.gridSize(image.*, t);
+ const rows_i32: i32 = @intCast(grid_size.rows);
+ const term_rows: i32 = @intCast(t.rows);
+ const visible = vp_row + rows_i32 > 0 and vp_row < term_rows;
+
+ return .{ .col = vp_col, .row = vp_row, .visible = visible };
+}
+
+test "placement_iterator new/free" {
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ try testing.expect(iter != null);
+ placement_iterator_free(iter);
+}
+
+test "placement_iterator free null" {
+ placement_iterator_free(null);
+}
+
+test "placement_iterator next on empty storage" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(!placement_iterator_next(iter));
+}
+
+test "placement_iterator get before next returns invalid" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+
+ var image_id: u32 = undefined;
+ try testing.expectEqual(Result.invalid_value, placement_get(iter, .image_id, @ptrCast(&image_id)));
+}
+
+test "placement_iterator with transmit and display" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ // Transmit and display a 1x2 RGB image (image_id=1, placement_id=1).
+ // a=T (transmit+display), t=d (direct), f=24 (RGB), i=1, p=1
+ // s=1,v=2 (1x2 pixels), c=10,r=1 (10 cols, 1 row)
+ // //////// = 8 base64 chars = 6 bytes = 1*2*3 RGB bytes
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+
+ // Should have exactly one placement.
+ try testing.expect(placement_iterator_next(iter));
+
+ var image_id: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_get(iter, .image_id, @ptrCast(&image_id)));
+ try testing.expectEqual(1, image_id);
+
+ var placement_id: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_get(iter, .placement_id, @ptrCast(&placement_id)));
+ try testing.expectEqual(1, placement_id);
+
+ var is_virtual: bool = undefined;
+ try testing.expectEqual(Result.success, placement_get(iter, .is_virtual, @ptrCast(&is_virtual)));
+ try testing.expect(!is_virtual);
+
+ // No more placements.
+ try testing.expect(!placement_iterator_next(iter));
+}
+
+test "placement_iterator with multiple placements" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ // Transmit image 1 then display it twice with different placement IDs.
+ const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
+ const display1 = "\x1b_Ga=p,i=1,p=1,c=10,r=1;\x1b\\";
+ const display2 = "\x1b_Ga=p,i=1,p=2,c=5,r=1;\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+ terminal_c.vt_write(t, display1.ptr, display1.len);
+ terminal_c.vt_write(t, display2.ptr, display2.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+
+ // Count placements and collect image IDs.
+ var count: usize = 0;
+ var seen_p1 = false;
+ var seen_p2 = false;
+ while (placement_iterator_next(iter)) {
+ count += 1;
+
+ var image_id: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_get(iter, .image_id, @ptrCast(&image_id)));
+ try testing.expectEqual(1, image_id);
+
+ var placement_id: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_get(iter, .placement_id, @ptrCast(&placement_id)));
+ if (placement_id == 1) seen_p1 = true;
+ if (placement_id == 2) seen_p2 = true;
+ }
+
+ try testing.expectEqual(2, count);
+ try testing.expect(seen_p1);
+ try testing.expect(seen_p2);
+}
+
+test "placement_iterator_set layer filter" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ // Transmit image 1.
+ const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+
+ // Display with z=5 (above text), z=-1 (below text), z=-1073741825 (below bg).
+ // INT32_MIN/2 = -1073741824, so -1073741825 < INT32_MIN/2.
+ const d1 = "\x1b_Ga=p,i=1,p=1,z=5;\x1b\\";
+ const d2 = "\x1b_Ga=p,i=1,p=2,z=-1;\x1b\\";
+ const d3 = "\x1b_Ga=p,i=1,p=3,z=-1073741825;\x1b\\";
+ terminal_c.vt_write(t, d1.ptr, d1.len);
+ terminal_c.vt_write(t, d2.ptr, d2.len);
+ terminal_c.vt_write(t, d3.ptr, d3.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ // Filter: above_text (z >= 0) — should yield only p=1.
+ var layer = PlacementLayer.above_text;
+ try testing.expectEqual(Result.success, placement_iterator_set(iter, .layer, @ptrCast(&layer)));
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+
+ var count: u32 = 0;
+ while (placement_iterator_next(iter)) {
+ var z: i32 = undefined;
+ try testing.expectEqual(Result.success, placement_get(iter, .z, @ptrCast(&z)));
+ try testing.expect(z >= 0);
+ count += 1;
+ }
+ try testing.expectEqual(1, count);
+
+ // Filter: below_text (INT32_MIN/2 <= z < 0) — should yield only p=2.
+ layer = .below_text;
+ try testing.expectEqual(Result.success, placement_iterator_set(iter, .layer, @ptrCast(&layer)));
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+
+ count = 0;
+ while (placement_iterator_next(iter)) {
+ var z: i32 = undefined;
+ try testing.expectEqual(Result.success, placement_get(iter, .z, @ptrCast(&z)));
+ try testing.expect(z >= std.math.minInt(i32) / 2 and z < 0);
+ count += 1;
+ }
+ try testing.expectEqual(1, count);
+
+ // Filter: below_bg (z < INT32_MIN/2) — should yield only p=3.
+ layer = .below_bg;
+ try testing.expectEqual(Result.success, placement_iterator_set(iter, .layer, @ptrCast(&layer)));
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+
+ count = 0;
+ while (placement_iterator_next(iter)) {
+ var z: i32 = undefined;
+ try testing.expectEqual(Result.success, placement_get(iter, .z, @ptrCast(&z)));
+ try testing.expect(z < std.math.minInt(i32) / 2);
+ count += 1;
+ }
+ try testing.expectEqual(1, count);
+
+ // Filter: all — should yield all 3.
+ layer = .all;
+ try testing.expectEqual(Result.success, placement_iterator_set(iter, .layer, @ptrCast(&layer)));
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+
+ count = 0;
+ while (placement_iterator_next(iter)) count += 1;
+ try testing.expectEqual(3, count);
+}
+
+test "image_get_handle returns null for missing id" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ try testing.expectEqual(@as(ImageHandle, null), image_get_handle(graphics, 999));
+}
+
+test "image_get_handle and image_get with transmitted image" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ // Transmit a 1x2 RGB image with image_id=1.
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var id: u32 = undefined;
+ try testing.expectEqual(Result.success, image_get(img, .id, @ptrCast(&id)));
+ try testing.expectEqual(1, id);
+
+ var w: u32 = undefined;
+ try testing.expectEqual(Result.success, image_get(img, .width, @ptrCast(&w)));
+ try testing.expectEqual(1, w);
+
+ var h: u32 = undefined;
+ try testing.expectEqual(Result.success, image_get(img, .height, @ptrCast(&h)));
+ try testing.expectEqual(2, h);
+
+ var fmt: ImageFormat = undefined;
+ try testing.expectEqual(Result.success, image_get(img, .format, @ptrCast(&fmt)));
+ try testing.expectEqual(.rgb, fmt);
+
+ var comp: ImageCompression = undefined;
+ try testing.expectEqual(Result.success, image_get(img, .compression, @ptrCast(&comp)));
+ try testing.expectEqual(.none, comp);
+
+ var data_len: usize = undefined;
+ try testing.expectEqual(Result.success, image_get(img, .data_len, @ptrCast(&data_len)));
+ try testing.expect(data_len > 0);
+}
+
+test "placement_rect with transmit and display" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ // Set cell size so grid calculations are deterministic.
+ // 80 cols * 10px = 800px, 24 rows * 20px = 480px.
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ // Transmit and display a 1x2 RGB image at cursor (0,0).
+ // c=10,r=1 => 10 columns, 1 row.
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var sel: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.success, placement_rect(iter, img, t, &sel));
+
+ // Placement starts at cursor origin (0,0).
+ try testing.expectEqual(0, sel.start.x);
+ try testing.expectEqual(0, sel.start.y);
+
+ // 10 columns wide, 1 row tall => bottom-right is (9, 0).
+ try testing.expectEqual(9, sel.end.x);
+ try testing.expectEqual(0, sel.end.y);
+
+ try testing.expect(sel.rectangle);
+}
+
+test "placement_rect null args return invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var sel: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.invalid_value, placement_rect(null, null, null, &sel));
+}
+
+test "placement_pixel_size with transmit and display" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ // 80 cols * 10px = 800px, 24 rows * 20px = 480px.
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ // Transmit and display a 1x2 RGB image with c=10,r=1.
+ // 10 cols * 10px = 100px width, 1 row * 20px = 20px height.
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var w: u32 = undefined;
+ var h: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_pixel_size(iter, img, t, &w, &h));
+
+ try testing.expectEqual(100, w);
+ try testing.expectEqual(20, h);
+}
+
+test "placement_pixel_size null args return invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var w: u32 = undefined;
+ var h: u32 = undefined;
+ try testing.expectEqual(Result.invalid_value, placement_pixel_size(null, null, null, &w, &h));
+}
+
+test "placement_grid_size with transmit and display" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ // 80 cols * 10px = 800px, 24 rows * 20px = 480px.
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ // Transmit and display a 1x2 RGB image with c=10,r=1.
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var cols: u32 = undefined;
+ var rows: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_grid_size(iter, img, t, &cols, &rows));
+
+ try testing.expectEqual(10, cols);
+ try testing.expectEqual(1, rows);
+}
+
+test "placement_grid_size null args return invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var cols: u32 = undefined;
+ var rows: u32 = undefined;
+ try testing.expectEqual(Result.invalid_value, placement_grid_size(null, null, null, &cols, &rows));
+}
+
+test "placement_viewport_pos with transmit and display" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ // Transmit and display at cursor (0,0).
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(
+ t,
+ .kitty_graphics,
+ @ptrCast(&graphics),
+ ));
+
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(
+ &lib.alloc.test_allocator,
+ &iter,
+ ));
+ defer placement_iterator_free(iter);
+
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var col: i32 = undefined;
+ var row: i32 = undefined;
+ try testing.expectEqual(Result.success, placement_viewport_pos(iter, img, t, &col, &row));
+
+ try testing.expectEqual(0, col);
+ try testing.expectEqual(0, row);
+}
+
+test "placement_viewport_pos fully off-screen above" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 5, .max_scrollback = 100 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20));
+
+ // Transmit image, then display at cursor (0,0) spanning 1 row.
+ const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
+ const display = "\x1b_Ga=p,i=1,p=1,c=1,r=1;\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+ terminal_c.vt_write(t, display.ptr, display.len);
+
+ // Scroll the image completely off: 10 newlines in a 5-row terminal
+ // scrolls by 5+ rows, so a 1-row image at row 0 is fully gone.
+ const scroll = "\n\n\n\n\n\n\n\n\n\n";
+ terminal_c.vt_write(t, scroll.ptr, scroll.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var col: i32 = undefined;
+ var row: i32 = undefined;
+ try testing.expectEqual(Result.no_value, placement_viewport_pos(iter, img, t, &col, &row));
+}
+
+test "placement_viewport_pos top off-screen" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 5, .max_scrollback = 100 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20));
+
+ // Transmit image, display at cursor (0,0) spanning 4 rows.
+ // C=1 prevents cursor movement after display.
+ const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
+ const display = "\x1b_Ga=p,i=1,p=1,c=1,r=4,C=1;\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+ terminal_c.vt_write(t, display.ptr, display.len);
+
+ // Scroll by 2: cursor starts at row 0, 4 newlines to reach bottom,
+ // then 2 more to scroll by 2. Image top-left moves to vp_row=-2,
+ // but bottom rows -2+4=2 > 0 so it's still partially visible.
+ const scroll = "\n\n\n\n\n\n";
+ terminal_c.vt_write(t, scroll.ptr, scroll.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var col: i32 = undefined;
+ var row: i32 = undefined;
+ try testing.expectEqual(Result.success, placement_viewport_pos(iter, img, t, &col, &row));
+ try testing.expectEqual(0, col);
+ try testing.expectEqual(-2, row);
+}
+
+test "placement_viewport_pos bottom off-screen" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 5, .max_scrollback = 0 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20));
+
+ // Transmit image, move cursor to row 3 (1-based: row 4), display spanning 4 rows.
+ // C=1 prevents cursor movement after display.
+ // Image occupies rows 3-6 but viewport only has rows 0-4, so bottom is clipped.
+ const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
+ const cursor = "\x1b[4;1H";
+ const display = "\x1b_Ga=p,i=1,p=1,c=1,r=4,C=1;\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+ terminal_c.vt_write(t, cursor.ptr, cursor.len);
+ terminal_c.vt_write(t, display.ptr, display.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var col: i32 = undefined;
+ var row: i32 = undefined;
+ try testing.expectEqual(Result.success, placement_viewport_pos(iter, img, t, &col, &row));
+ try testing.expectEqual(0, col);
+ try testing.expectEqual(3, row);
+}
+
+test "placement_viewport_pos top and bottom off-screen" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 5, .max_scrollback = 100 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20));
+
+ // Transmit image, display at cursor (0,0) spanning 10 rows.
+ // C=1 prevents cursor movement after display.
+ // After scrolling by 3, image occupies vp rows -3..6, viewport is 0..4,
+ // so both top and bottom are clipped but center is visible.
+ const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
+ const display = "\x1b_Ga=p,i=1,p=1,c=1,r=10,C=1;\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+ terminal_c.vt_write(t, display.ptr, display.len);
+
+ // Scroll by 3: 4 newlines to reach bottom + 3 more to scroll.
+ const scroll = "\n\n\n\n\n\n\n";
+ terminal_c.vt_write(t, scroll.ptr, scroll.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var col: i32 = undefined;
+ var row: i32 = undefined;
+ try testing.expectEqual(Result.success, placement_viewport_pos(iter, img, t, &col, &row));
+ try testing.expectEqual(0, col);
+ try testing.expectEqual(-3, row);
+}
+
+test "placement_viewport_pos null args return invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var col: i32 = undefined;
+ var row: i32 = undefined;
+ try testing.expectEqual(Result.invalid_value, placement_viewport_pos(null, null, null, &col, &row));
+}
+
+test "placement_source_rect defaults to full image" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ // Transmit and display a 1x2 RGB image with no source rect specified.
+ // source_width=0 and source_height=0 should resolve to full image (1x2).
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var x: u32 = undefined;
+ var y: u32 = undefined;
+ var w: u32 = undefined;
+ var h: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_source_rect(iter, img, &x, &y, &w, &h));
+ try testing.expectEqual(0, x);
+ try testing.expectEqual(0, y);
+ try testing.expectEqual(1, w);
+ try testing.expectEqual(2, h);
+}
+
+test "placement_source_rect with explicit source rect" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ // Transmit a 4x4 RGBA image (64 bytes = 4*4*4).
+ // Base64 of 64 zero bytes: 88 chars (21 full groups + AA== padding).
+ const transmit = "\x1b_Ga=t,t=d,f=32,i=1,s=4,v=4;" ++
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" ++
+ "\x1b\\";
+ // Display with explicit source rect: x=1, y=1, w=2, h=2.
+ const display = "\x1b_Ga=p,i=1,p=1,x=1,y=1,w=2,h=2;\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+ terminal_c.vt_write(t, display.ptr, display.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var x: u32 = undefined;
+ var y: u32 = undefined;
+ var w: u32 = undefined;
+ var h: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_source_rect(iter, img, &x, &y, &w, &h));
+ try testing.expectEqual(1, x);
+ try testing.expectEqual(1, y);
+ try testing.expectEqual(2, w);
+ try testing.expectEqual(2, h);
+}
+
+test "placement_source_rect clamps to image bounds" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ // Transmit a 4x4 RGBA image (64 bytes = 4*4*4).
+ const transmit = "\x1b_Ga=t,t=d,f=32,i=1,s=4,v=4;" ++
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" ++
+ "\x1b\\";
+ // Display with source rect that exceeds image bounds: x=3, y=3, w=10, h=10.
+ // Should clamp to x=3, y=3, w=1, h=1.
+ const display = "\x1b_Ga=p,i=1,p=1,x=3,y=3,w=10,h=10;\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+ terminal_c.vt_write(t, display.ptr, display.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var x: u32 = undefined;
+ var y: u32 = undefined;
+ var w: u32 = undefined;
+ var h: u32 = undefined;
+ try testing.expectEqual(Result.success, placement_source_rect(iter, img, &x, &y, &w, &h));
+ try testing.expectEqual(3, x);
+ try testing.expectEqual(3, y);
+ try testing.expectEqual(1, w);
+ try testing.expectEqual(1, h);
+}
+
+test "placement_source_rect null args return invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var x: u32 = undefined;
+ var y: u32 = undefined;
+ var w: u32 = undefined;
+ var h: u32 = undefined;
+ try testing.expectEqual(Result.invalid_value, placement_source_rect(null, null, &x, &y, &w, &h));
+}
+
+test "image_get on null returns invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var id: u32 = undefined;
+ try testing.expectEqual(Result.invalid_value, image_get(null, .id, @ptrCast(&id)));
+}
+
+test "placement_render_info returns all fields" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var ri: PlacementRenderInfo = .{};
+ try testing.expectEqual(Result.success, placement_render_info(iter, img, t, &ri));
+ try testing.expect(ri.viewport_visible);
+ try testing.expectEqual(0, ri.viewport_col);
+ try testing.expectEqual(0, ri.viewport_row);
+ try testing.expectEqual(10, ri.grid_cols);
+ try testing.expectEqual(1, ri.grid_rows);
+ try testing.expectEqual(0, ri.source_x);
+ try testing.expectEqual(0, ri.source_y);
+ try testing.expectEqual(1, ri.source_width);
+ try testing.expectEqual(2, ri.source_height);
+}
+
+test "placement_render_info off-screen sets viewport_visible false" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 5, .max_scrollback = 100 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 5, 10, 20));
+
+ const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
+ const display = "\x1b_Ga=p,i=1,p=1,c=1,r=1;\x1b\\";
+ terminal_c.vt_write(t, transmit.ptr, transmit.len);
+ terminal_c.vt_write(t, display.ptr, display.len);
+
+ // Scroll the image completely off-screen.
+ const scroll = "\n\n\n\n\n\n\n\n\n\n";
+ terminal_c.vt_write(t, scroll.ptr, scroll.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var ri: PlacementRenderInfo = .{};
+ try testing.expectEqual(Result.success, placement_render_info(iter, img, t, &ri));
+ try testing.expect(!ri.viewport_visible);
+ // Other fields should still be populated.
+ try testing.expectEqual(1, ri.grid_cols);
+ try testing.expectEqual(1, ri.grid_rows);
+ try testing.expectEqual(1, ri.source_width);
+ try testing.expectEqual(2, ri.source_height);
+}
+
+test "placement_render_info null returns invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var ri: PlacementRenderInfo = .{};
+ try testing.expectEqual(Result.invalid_value, placement_render_info(null, null, null, &ri));
+}
+
+test "image_get_multi success" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+ const img = image_get_handle(graphics, 1);
+ try testing.expect(img != null);
+
+ var id: u32 = 0;
+ var width: u32 = 0;
+ var height: u32 = 0;
+ var written: usize = 0;
+
+ const keys = [_]ImageData{ .id, .width, .height };
+ var values = [_]?*anyopaque{ @ptrCast(&id), @ptrCast(&width), @ptrCast(&height) };
+ try testing.expectEqual(Result.success, image_get_multi(img, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+ try testing.expectEqual(1, id);
+ try testing.expectEqual(1, width);
+ try testing.expectEqual(2, height);
+}
+
+test "image_get_multi error sets out_written" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var id: u32 = 0;
+ var written: usize = 99;
+
+ const keys = [_]ImageData{ .id, .invalid };
+ var values = [_]?*anyopaque{ @ptrCast(&id), @ptrCast(&id) };
+ try testing.expectEqual(Result.invalid_value, image_get_multi(null, keys.len, &keys, &values, &written));
+ try testing.expectEqual(0, written);
+}
+
+test "image_get_multi null keys returns invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var id: u32 = 0;
+ var values = [_]?*anyopaque{@ptrCast(&id)};
+ try testing.expectEqual(Result.invalid_value, image_get_multi(null, 1, null, &values, null));
+}
+
+test "placement_get_multi success" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ 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 = 0 },
+ ));
+ defer terminal_c.free(t);
+ try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
+
+ const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
+ terminal_c.vt_write(t, cmd.ptr, cmd.len);
+
+ var graphics: KittyGraphics = undefined;
+ try testing.expectEqual(Result.success, terminal_c.get(t, .kitty_graphics, @ptrCast(&graphics)));
+
+ var iter: PlacementIterator = null;
+ try testing.expectEqual(Result.success, placement_iterator_new(&lib.alloc.test_allocator, &iter));
+ defer placement_iterator_free(iter);
+ try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
+ try testing.expect(placement_iterator_next(iter));
+
+ var image_id: u32 = 0;
+ var columns: u32 = 0;
+ var z: i32 = 99;
+ var written: usize = 0;
+
+ const keys = [_]PlacementData{ .image_id, .columns, .z };
+ var values = [_]?*anyopaque{ @ptrCast(&image_id), @ptrCast(&columns), @ptrCast(&z) };
+ try testing.expectEqual(Result.success, placement_get_multi(iter, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+ try testing.expectEqual(1, image_id);
+ try testing.expectEqual(10, columns);
+ try testing.expectEqual(0, z);
+}
+
+test "placement_get_multi error sets out_written" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var id: u32 = 0;
+ var written: usize = 99;
+
+ const keys = [_]PlacementData{ .image_id, .invalid };
+ var values = [_]?*anyopaque{ @ptrCast(&id), @ptrCast(&id) };
+ try testing.expectEqual(Result.invalid_value, placement_get_multi(null, keys.len, &keys, &values, &written));
+ try testing.expectEqual(0, written);
+}
+
+test "placement_get_multi null keys returns invalid_value" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var id: u32 = 0;
+ var values = [_]?*anyopaque{@ptrCast(&id)};
+ try testing.expectEqual(Result.invalid_value, placement_get_multi(null, 1, null, &values, null));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/main.zig b/vendor/libghostty-vt/src/terminal/c/main.zig
index 34d1a8f4..126b6640 100644
--- a/vendor/libghostty-vt/src/terminal/c/main.zig
+++ b/vendor/libghostty-vt/src/terminal/c/main.zig
@@ -8,10 +8,28 @@ 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 kitty_graphics = @import("kitty_graphics.zig");
+pub const kitty_graphics_get = kitty_graphics.get;
+pub const kitty_graphics_image = kitty_graphics.image_get_handle;
+pub const kitty_graphics_image_get = kitty_graphics.image_get;
+pub const kitty_graphics_image_get_multi = kitty_graphics.image_get_multi;
+pub const kitty_graphics_placement_iterator_new = kitty_graphics.placement_iterator_new;
+pub const kitty_graphics_placement_iterator_free = kitty_graphics.placement_iterator_free;
+pub const kitty_graphics_placement_iterator_set = kitty_graphics.placement_iterator_set;
+pub const kitty_graphics_placement_next = kitty_graphics.placement_iterator_next;
+pub const kitty_graphics_placement_get = kitty_graphics.placement_get;
+pub const kitty_graphics_placement_get_multi = kitty_graphics.placement_get_multi;
+pub const kitty_graphics_placement_rect = kitty_graphics.placement_rect;
+pub const kitty_graphics_placement_pixel_size = kitty_graphics.placement_pixel_size;
+pub const kitty_graphics_placement_grid_size = kitty_graphics.placement_grid_size;
+pub const kitty_graphics_placement_viewport_pos = kitty_graphics.placement_viewport_pos;
+pub const kitty_graphics_placement_source_rect = kitty_graphics.placement_source_rect;
+pub const kitty_graphics_placement_render_info = kitty_graphics.placement_render_info;
pub const types = @import("types.zig");
pub const modes = @import("modes.zig");
pub const osc = @import("osc.zig");
pub const render = @import("render.zig");
+pub const selection = @import("selection.zig");
pub const key_event = @import("key_event.zig");
pub const key_encode = @import("key_encode.zig");
pub const mouse_event = @import("mouse_event.zig");
@@ -21,6 +39,7 @@ pub const row = @import("row.zig");
pub const sgr = @import("sgr.zig");
pub const size_report = @import("size_report.zig");
pub const style = @import("style.zig");
+pub const sys = @import("sys.zig");
pub const terminal = @import("terminal.zig");
// The full C API, unexported.
@@ -49,17 +68,20 @@ pub const render_state_new = render.new;
pub const render_state_free = render.free;
pub const render_state_update = render.update;
pub const render_state_get = render.get;
+pub const render_state_get_multi = render.get_multi;
pub const render_state_set = render.set;
pub const render_state_colors_get = render.colors_get;
pub const render_state_row_iterator_new = render.row_iterator_new;
pub const render_state_row_iterator_next = render.row_iterator_next;
pub const render_state_row_get = render.row_get;
+pub const render_state_row_get_multi = render.row_get_multi;
pub const render_state_row_set = render.row_set;
pub const render_state_row_iterator_free = render.row_iterator_free;
pub const render_state_row_cells_new = render.row_cells_new;
pub const render_state_row_cells_next = render.row_cells_next;
pub const render_state_row_cells_select = render.row_cells_select;
pub const render_state_row_cells_get = render.row_cells_get;
+pub const render_state_row_cells_get_multi = render.row_cells_get_multi;
pub const render_state_row_cells_free = render.row_cells_free;
pub const sgr_new = sgr.new;
@@ -125,12 +147,17 @@ pub const alloc_free = allocator.free;
pub const size_report_encode = size_report.encode;
pub const cell_get = cell.get;
+pub const cell_get_multi = cell.get_multi;
pub const row_get = row.get;
+pub const row_get_multi = row.get_multi;
pub const style_default = style.default_style;
pub const style_is_default = style.style_is_default;
+pub const sys_log_stderr = sys.logStderr;
+pub const sys_set = sys.set;
+
pub const terminal_new = terminal.new;
pub const terminal_free = terminal.free;
pub const terminal_reset = terminal.reset;
@@ -141,14 +168,16 @@ pub const terminal_scroll_viewport = terminal.scroll_viewport;
pub const terminal_mode_get = terminal.mode_get;
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_read_text = terminal.read_text;
+pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref;
pub const type_json = types.get_json;
pub const grid_ref_cell = grid_ref.grid_ref_cell;
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;
test {
@@ -157,12 +186,14 @@ test {
_ = cell;
_ = color;
_ = grid_ref;
+ _ = kitty_graphics;
_ = row;
_ = focus;
_ = formatter;
_ = modes;
_ = osc;
_ = render;
+ _ = selection;
_ = key_event;
_ = key_encode;
_ = mouse_event;
@@ -171,6 +202,7 @@ test {
_ = sgr;
_ = size_report;
_ = style;
+ _ = sys;
_ = terminal;
_ = types;
diff --git a/vendor/libghostty-vt/src/terminal/c/render.zig b/vendor/libghostty-vt/src/terminal/c/render.zig
index b95bedd5..af82ddfa 100644
--- a/vendor/libghostty-vt/src/terminal/c/render.zig
+++ b/vendor/libghostty-vt/src/terminal/c/render.zig
@@ -201,6 +201,27 @@ pub fn get(
};
}
+pub fn get_multi(
+ state_: RenderState,
+ count: usize,
+ keys: ?[*]const Data,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = get(state_, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
fn getTyped(
state_: RenderState,
comptime data: Data,
@@ -468,6 +489,27 @@ pub fn row_cells_get(
};
}
+pub fn row_cells_get_multi(
+ cells_: RowCells,
+ count: usize,
+ keys: ?[*]const RowCellsData,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = row_cells_get(cells_, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
fn rowCellsGetTyped(
cells_: RowCells,
comptime data: RowCellsData,
@@ -568,6 +610,27 @@ pub fn row_get(
};
}
+pub fn row_get_multi(
+ iterator_: RowIterator,
+ count: usize,
+ keys: ?[*]const RowData,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = row_get(iterator_, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
fn rowGetTyped(
iterator_: RowIterator,
comptime data: RowData,
@@ -1380,3 +1443,137 @@ test "render: colors get supports truncated sized struct" {
try testing.expectEqual(state_colors.palette[1].cval(), colors.palette[1]);
try testing.expectEqual(sentinel, colors.palette[2]);
}
+
+test "render: get_multi success" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var cols: u16 = 0;
+ var rows: u16 = 0;
+ var written: usize = 0;
+
+ const keys = [_]Data{ .cols, .rows };
+ var values = [_]?*anyopaque{ @ptrCast(&cols), @ptrCast(&rows) };
+ try testing.expectEqual(Result.success, get_multi(state, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+ try testing.expectEqual(80, cols);
+ try testing.expectEqual(24, rows);
+}
+
+test "render: get_multi null returns invalid_value" {
+ var cols: u16 = 0;
+ var values = [_]?*anyopaque{@ptrCast(&cols)};
+ try testing.expectEqual(Result.invalid_value, get_multi(null, 1, null, &values, null));
+}
+
+test "render: row_get_multi success" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(terminal);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+
+ var dirty: bool = true;
+ var written: usize = 0;
+
+ const keys = [_]RowData{.dirty};
+ var values = [_]?*anyopaque{@ptrCast(&dirty)};
+ try testing.expectEqual(Result.success, row_get_multi(it, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+}
+
+test "render: row_get_multi null returns invalid_value" {
+ var dirty: bool = false;
+ var values = [_]?*anyopaque{@ptrCast(&dirty)};
+ try testing.expectEqual(Result.invalid_value, row_get_multi(null, 1, null, &values, null));
+}
+
+test "render: row_cells_get_multi success" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(terminal);
+
+ terminal_c.vt_write(terminal, "A", 1);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+ try testing.expect(row_cells_next(cells));
+
+ var raw: row.CRow = undefined;
+ var written: usize = 0;
+
+ const keys = [_]RowCellsData{.raw};
+ var values = [_]?*anyopaque{@ptrCast(&raw)};
+ try testing.expectEqual(Result.success, row_cells_get_multi(cells, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+}
+
+test "render: row_cells_get_multi null returns invalid_value" {
+ var raw: row.CRow = undefined;
+ var values = [_]?*anyopaque{@ptrCast(&raw)};
+ try testing.expectEqual(Result.invalid_value, row_cells_get_multi(null, 1, null, &values, null));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/row.zig b/vendor/libghostty-vt/src/terminal/c/row.zig
index d4532706..66c398c8 100644
--- a/vendor/libghostty-vt/src/terminal/c/row.zig
+++ b/vendor/libghostty-vt/src/terminal/c/row.zig
@@ -83,6 +83,27 @@ pub fn get(
};
}
+pub fn get_multi(
+ row_: CRow,
+ count: usize,
+ keys: ?[*]const RowData,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = get(row_, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
fn getTyped(
row_: CRow,
comptime data: RowData,
@@ -130,3 +151,43 @@ test "get dirty" {
try testing.expectEqual(Result.success, get(row, .dirty, @ptrCast(&dirty)));
try testing.expect(dirty);
}
+
+test "get_multi success" {
+ var zig_row: Row = @bitCast(@as(u64, 0));
+ zig_row.wrap = true;
+ zig_row.dirty = true;
+ const row_val: CRow = @bitCast(zig_row);
+
+ var wrap: bool = false;
+ var dirty: bool = false;
+ var written: usize = 0;
+
+ const keys = [_]RowData{ .wrap, .dirty };
+ var values = [_]?*anyopaque{ @ptrCast(&wrap), @ptrCast(&dirty) };
+ try testing.expectEqual(Result.success, get_multi(row_val, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+ try testing.expect(wrap);
+ try testing.expect(dirty);
+}
+
+test "get_multi error sets out_written" {
+ var zig_row: Row = @bitCast(@as(u64, 0));
+ zig_row.wrap = true;
+ const row_val: CRow = @bitCast(zig_row);
+
+ var wrap: bool = false;
+ var written: usize = 99;
+
+ const keys = [_]RowData{ .wrap, .invalid };
+ var values = [_]?*anyopaque{ @ptrCast(&wrap), @ptrCast(&wrap) };
+ try testing.expectEqual(Result.invalid_value, get_multi(row_val, keys.len, &keys, &values, &written));
+ try testing.expectEqual(1, written);
+ try testing.expect(wrap);
+}
+
+test "get_multi null keys returns invalid_value" {
+ const row_val: CRow = @bitCast(@as(u64, 0));
+ var wrap: bool = false;
+ var values = [_]?*anyopaque{@ptrCast(&wrap)};
+ try testing.expectEqual(Result.invalid_value, get_multi(row_val, 1, null, &values, null));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/selection.zig b/vendor/libghostty-vt/src/terminal/c/selection.zig
new file mode 100644
index 00000000..74e96598
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/selection.zig
@@ -0,0 +1,16 @@
+const grid_ref = @import("grid_ref.zig");
+const Selection = @import("../Selection.zig");
+
+/// C: GhosttySelection
+pub const CSelection = extern struct {
+ size: usize = @sizeOf(CSelection),
+ start: grid_ref.CGridRef,
+ end: grid_ref.CGridRef,
+ rectangle: bool = false,
+
+ pub fn toZig(self: CSelection) ?Selection {
+ const start_pin = self.start.toPin() orelse return null;
+ const end_pin = self.end.toPin() orelse return null;
+ return Selection.init(start_pin, end_pin, self.rectangle);
+ }
+};
diff --git a/vendor/libghostty-vt/src/terminal/c/sys.zig b/vendor/libghostty-vt/src/terminal/c/sys.zig
new file mode 100644
index 00000000..c4b2b17f
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/sys.zig
@@ -0,0 +1,430 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const terminal_sys = @import("../sys.zig");
+const Result = @import("result.zig").Result;
+
+/// C: GhosttySysImage
+pub const Image = extern struct {
+ width: u32,
+ height: u32,
+ data: ?[*]u8,
+ data_len: usize,
+};
+
+/// C: GhosttySysDecodePngFn
+pub const DecodePngFn = *const fn (
+ ?*anyopaque,
+ *const CAllocator,
+ [*]const u8,
+ usize,
+ *Image,
+) callconv(lib.calling_conv) bool;
+
+/// C: GhosttySysLogLevel
+pub const LogLevel = enum(c_int) {
+ @"error" = 0,
+ warning = 1,
+ info = 2,
+ debug = 3,
+
+ pub fn fromStd(level: std.log.Level) LogLevel {
+ return switch (level) {
+ .err => .@"error",
+ .warn => .warning,
+ .info => .info,
+ .debug => .debug,
+ };
+ }
+};
+
+/// C: GhosttySysLogFn
+pub const LogFn = *const fn (
+ ?*anyopaque,
+ LogLevel,
+ [*]const u8,
+ usize,
+ [*]const u8,
+ usize,
+) callconv(lib.calling_conv) void;
+
+/// C: GhosttySysOption
+pub const Option = enum(c_int) {
+ userdata = 0,
+ decode_png = 1,
+ log = 2,
+
+ pub fn InType(comptime self: Option) type {
+ return switch (self) {
+ .userdata => ?*const anyopaque,
+ .decode_png => ?DecodePngFn,
+ .log => ?LogFn,
+ };
+ }
+};
+
+/// Global state for the sys interface so we can call through to the C
+/// callbacks from Zig.
+const Global = struct {
+ userdata: ?*anyopaque = null,
+ decode_png: ?DecodePngFn = null,
+ log: ?LogFn = null,
+};
+
+/// Global state for the C sys interface.
+var global: Global = .{};
+
+/// Zig-compatible wrapper that calls through to the stored C callback.
+/// The C callback allocates the pixel data through the provided allocator,
+/// so we can take ownership directly.
+fn decodePngWrapper(
+ alloc: std.mem.Allocator,
+ data: []const u8,
+) terminal_sys.DecodeError!terminal_sys.Image {
+ const func = global.decode_png orelse return error.InvalidData;
+
+ const c_alloc = CAllocator.fromZig(&alloc);
+ var out: Image = undefined;
+ if (!func(global.userdata, &c_alloc, data.ptr, data.len, &out)) return error.InvalidData;
+
+ const result_data = out.data orelse return error.InvalidData;
+
+ return .{
+ .width = out.width,
+ .height = out.height,
+ .data = result_data[0..out.data_len],
+ };
+}
+
+pub fn set(
+ option: Option,
+ value: ?*const anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
+ return .invalid_value;
+ };
+ }
+
+ return switch (option) {
+ inline else => |comptime_option| setTyped(
+ comptime_option,
+ @ptrCast(@alignCast(value)),
+ ),
+ };
+}
+
+fn setTyped(
+ comptime option: Option,
+ value: option.InType(),
+) Result {
+ switch (option) {
+ .userdata => global.userdata = @constCast(value),
+ .decode_png => {
+ global.decode_png = value;
+ terminal_sys.decode_png = if (value != null) &decodePngWrapper else null;
+ },
+ .log => global.log = value,
+ }
+ return .success;
+}
+
+/// Dispatch a log message to the installed C callback, if any.
+fn emitLog(level: LogLevel, scope: []const u8, message: []const u8) void {
+ const func = global.log orelse return;
+ func(
+ global.userdata,
+ level,
+ scope.ptr,
+ scope.len,
+ message.ptr,
+ message.len,
+ );
+}
+
+/// Emits logs in chunks. Almost all logs will be less than the chunk size
+/// but this allows emitting larger logs without heap allocation.
+const LogEmitter = struct {
+ c_level: LogLevel,
+ scope_text: []const u8,
+ buf: [2048]u8 = undefined,
+ pos: usize = 0,
+
+ fn write(self: *@This(), bytes: []const u8) error{}!usize {
+ var remaining = bytes;
+ while (remaining.len > 0) {
+ const space = self.buf.len - self.pos;
+ if (space == 0) {
+ self.flush();
+ continue;
+ }
+
+ const n = @min(remaining.len, space);
+ @memcpy(self.buf[self.pos..][0..n], remaining[0..n]);
+ self.pos += n;
+ remaining = remaining[n..];
+ }
+
+ return bytes.len;
+ }
+
+ fn flush(self: *@This()) void {
+ if (self.pos == 0) return;
+ emitLog(
+ self.c_level,
+ self.scope_text,
+ self.buf[0..self.pos],
+ );
+ self.pos = 0;
+ }
+};
+
+/// Custom std.log sink for C ABI builds.
+///
+/// When a log callback is installed via ghostty_sys_set(), messages are
+/// dispatched through it. When no callback is installed, messages are
+/// silently discarded. Large messages that exceed the stack buffer are
+/// delivered across multiple callback invocations.
+pub fn logFn(
+ comptime level: std.log.Level,
+ comptime scope: @TypeOf(.EnumLiteral),
+ comptime format: []const u8,
+ args: anytype,
+) void {
+ if (global.log == null) return;
+
+ const scope_text: []const u8 = if (scope == .default) "" else @tagName(scope);
+ const c_level = LogLevel.fromStd(level);
+
+ var ctx: LogEmitter = .{
+ .c_level = c_level,
+ .scope_text = scope_text,
+ };
+ const writer: std.io.GenericWriter(
+ *LogEmitter,
+ error{},
+ LogEmitter.write,
+ ) = .{ .context = &ctx };
+
+ nosuspend writer.print(format, args) catch {};
+ ctx.flush();
+}
+
+/// Built-in log callback that writes to stderr.
+///
+/// Formats each message as "[level](scope): message\n". Can be passed
+/// directly to ghostty_sys_set(GHOSTTY_SYS_OPT_LOG, &ghostty_sys_log_stderr).
+///
+/// Uses std.debug.lockStderrWriter for thread-safe, mutex-protected output.
+/// On freestanding/wasm targets this is a no-op (no stderr available).
+pub fn logStderr(
+ _: ?*anyopaque,
+ level: LogLevel,
+ scope_ptr: [*]const u8,
+ scope_len: usize,
+ message_ptr: [*]const u8,
+ message_len: usize,
+) callconv(lib.calling_conv) void {
+ if (comptime builtin.target.cpu.arch.isWasm()) return;
+
+ const scope = scope_ptr[0..scope_len];
+ const message = message_ptr[0..message_len];
+
+ const level_text = switch (level) {
+ .@"error" => "error",
+ .warning => "warning",
+ .info => "info",
+ .debug => "debug",
+ };
+
+ var buffer: [64]u8 = undefined;
+ const writer = std.debug.lockStderrWriter(&buffer);
+ defer std.debug.unlockStderrWriter();
+ nosuspend {
+ if (scope.len > 0) {
+ writer.print("[{s}]({s}): {s}\n", .{ level_text, scope, message }) catch {};
+ } else {
+ writer.print("[{s}]: {s}\n", .{ level_text, message }) catch {};
+ }
+ }
+}
+
+test "set decode_png with null clears" {
+ // Start from a known state.
+ global.decode_png = null;
+ terminal_sys.decode_png = null;
+
+ try std.testing.expectEqual(Result.success, set(.decode_png, null));
+ try std.testing.expect(terminal_sys.decode_png == null);
+}
+
+test "set decode_png installs wrapper" {
+ const S = struct {
+ fn decode(_: ?*anyopaque, _: *const CAllocator, _: [*]const u8, _: usize, out: *Image) callconv(lib.calling_conv) bool {
+ out.* = .{ .width = 1, .height = 1, .data = null, .data_len = 0 };
+ return true;
+ }
+ };
+
+ try std.testing.expectEqual(Result.success, set(
+ .decode_png,
+ @ptrCast(&S.decode),
+ ));
+ try std.testing.expect(terminal_sys.decode_png != null);
+
+ // Clear it again.
+ try std.testing.expectEqual(Result.success, set(.decode_png, null));
+ try std.testing.expect(terminal_sys.decode_png == null);
+}
+
+test "set log with null clears" {
+ global.log = null;
+
+ try std.testing.expectEqual(Result.success, set(.log, null));
+ try std.testing.expect(global.log == null);
+}
+
+test "set log installs callback" {
+ const S = struct {
+ var called: bool = false;
+ fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, _: [*]const u8, _: usize) callconv(lib.calling_conv) void {
+ called = true;
+ }
+ };
+
+ try std.testing.expectEqual(Result.success, set(.log, @ptrCast(&S.logCb)));
+ try std.testing.expect(global.log != null);
+
+ emitLog(.info, "test", "hello");
+ try std.testing.expect(S.called);
+
+ // Clear it again.
+ S.called = false;
+ try std.testing.expectEqual(Result.success, set(.log, null));
+ try std.testing.expect(global.log == null);
+
+ emitLog(.info, "test", "should not arrive");
+ try std.testing.expect(!S.called);
+}
+
+test "logFn small message single chunk" {
+ const S = struct {
+ var call_count: usize = 0;
+ var total_len: usize = 0;
+
+ fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void {
+ _ = msg;
+ call_count += 1;
+ total_len += msg_len;
+ }
+ };
+
+ S.call_count = 0;
+ S.total_len = 0;
+ global.log = @ptrCast(&S.logCb);
+ defer {
+ global.log = null;
+ }
+
+ logFn(.info, .default, "hello", .{});
+
+ try std.testing.expectEqual(@as(usize, 1), S.call_count);
+ try std.testing.expectEqual(@as(usize, 5), S.total_len);
+}
+
+test "logFn message exceeding chunk size is split" {
+ const S = struct {
+ var call_count: usize = 0;
+ var total_len: usize = 0;
+
+ fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void {
+ _ = msg;
+ call_count += 1;
+ total_len += msg_len;
+ // Each chunk must not exceed the buffer size.
+ std.debug.assert(msg_len <= 2048);
+ }
+ };
+
+ S.call_count = 0;
+ S.total_len = 0;
+ global.log = @ptrCast(&S.logCb);
+ defer {
+ global.log = null;
+ }
+
+ // Format a message larger than the 2048-byte buffer.
+ // 'A' repeated 3000 times via a fill format.
+ const fill: [3000]u8 = .{0x41} ** 3000;
+ logFn(.info, .default, "{s}", .{@as([]const u8, &fill)});
+
+ try std.testing.expect(S.call_count >= 2);
+ try std.testing.expectEqual(@as(usize, 3000), S.total_len);
+}
+
+test "logFn message exactly at chunk boundary" {
+ const S = struct {
+ var call_count: usize = 0;
+ var total_len: usize = 0;
+
+ fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void {
+ _ = msg;
+ call_count += 1;
+ total_len += msg_len;
+ std.debug.assert(msg_len <= 2048);
+ }
+ };
+
+ S.call_count = 0;
+ S.total_len = 0;
+ global.log = @ptrCast(&S.logCb);
+ defer {
+ global.log = null;
+ }
+
+ // Exactly 2048 bytes — should emit one full chunk, no remainder.
+ const fill: [2048]u8 = .{0x42} ** 2048;
+ logFn(.info, .default, "{s}", .{@as([]const u8, &fill)});
+
+ try std.testing.expectEqual(@as(usize, 1), S.call_count);
+ try std.testing.expectEqual(@as(usize, 2048), S.total_len);
+}
+
+test "logFn message exactly double chunk size" {
+ const S = struct {
+ var call_count: usize = 0;
+ var total_len: usize = 0;
+
+ fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void {
+ _ = msg;
+ call_count += 1;
+ total_len += msg_len;
+ std.debug.assert(msg_len <= 2048);
+ }
+ };
+
+ S.call_count = 0;
+ S.total_len = 0;
+ global.log = @ptrCast(&S.logCb);
+ defer {
+ global.log = null;
+ }
+
+ // Exactly 4096 bytes — should emit exactly two full chunks.
+ const fill: [4096]u8 = .{0x43} ** 4096;
+ logFn(.info, .default, "{s}", .{@as([]const u8, &fill)});
+
+ try std.testing.expectEqual(@as(usize, 2), S.call_count);
+ try std.testing.expectEqual(@as(usize, 4096), S.total_len);
+}
+
+test "set userdata" {
+ var data: u32 = 42;
+ try std.testing.expectEqual(Result.success, set(.userdata, @ptrCast(&data)));
+ try std.testing.expect(global.userdata == @as(?*anyopaque, @ptrCast(&data)));
+
+ // Clear it.
+ try std.testing.expectEqual(Result.success, set(.userdata, null));
+ try std.testing.expect(global.userdata == null);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/terminal.zig b/vendor/libghostty-vt/src/terminal/c/terminal.zig
index fa421f21..d3293f3b 100644
--- a/vendor/libghostty-vt/src/terminal/c/terminal.zig
+++ b/vendor/libghostty-vt/src/terminal/c/terminal.zig
@@ -1,20 +1,21 @@
const std = @import("std");
const testing = std.testing;
+const build_options = @import("terminal_options");
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const ZigTerminal = @import("../Terminal.zig");
const Stream = @import("../stream_terminal.zig").Stream;
const ScreenSet = @import("../ScreenSet.zig");
const PageList = @import("../PageList.zig");
+const apc = @import("../apc.zig");
const kitty = @import("../kitty/key.zig");
+const kitty_gfx_c = @import("kitty_graphics.zig");
const modes = @import("../modes.zig");
const point = @import("../point.zig");
const size = @import("../size.zig");
const device_attributes = @import("../device_attributes.zig");
const device_status = @import("../device_status.zig");
const size_report = @import("../size_report.zig");
-const formatterpkg = @import("../formatter.zig");
-const selectionpkg = @import("../Selection.zig");
const cell_c = @import("cell.zig");
const row_c = @import("row.zig");
const grid_ref_c = @import("grid_ref.zig");
@@ -306,6 +307,12 @@ pub const Option = enum(c_int) {
color_background = 12,
color_cursor = 13,
color_palette = 14,
+ kitty_image_storage_limit = 15,
+ kitty_image_medium_file = 16,
+ kitty_image_medium_temp_file = 17,
+ kitty_image_medium_shared_mem = 18,
+ apc_max_bytes = 19,
+ apc_max_bytes_kitty = 20,
/// Input type expected for setting the option.
pub fn InType(comptime self: Option) type {
@@ -322,6 +329,12 @@ pub const Option = enum(c_int) {
.title, .pwd => ?*const lib.String,
.color_foreground, .color_background, .color_cursor => ?*const color.RGB.C,
.color_palette => ?*const color.PaletteC,
+ .kitty_image_storage_limit => ?*const u64,
+ .kitty_image_medium_file,
+ .kitty_image_medium_temp_file,
+ .kitty_image_medium_shared_mem,
+ => ?*const bool,
+ .apc_max_bytes, .apc_max_bytes_kitty => ?*const usize,
};
}
};
@@ -390,6 +403,45 @@ fn setTyped(
);
wrapper.terminal.flags.dirty.palette = true;
},
+ .kitty_image_storage_limit => {
+ if (comptime !build_options.kitty_graphics) return .success;
+ const limit: usize = if (value) |v| @intCast(v.*) else 0;
+ var it = wrapper.terminal.screens.all.iterator();
+ while (it.next()) |entry| {
+ const screen = entry.value.*;
+ screen.kitty_images.setLimit(screen.alloc, screen, limit) catch return .out_of_memory;
+ }
+ },
+ .kitty_image_medium_file,
+ .kitty_image_medium_temp_file,
+ .kitty_image_medium_shared_mem,
+ => {
+ if (comptime !build_options.kitty_graphics) return .success;
+ const val = (value orelse return .success).*;
+ var it = wrapper.terminal.screens.all.iterator();
+ while (it.next()) |entry| {
+ const screen = entry.value.*;
+ switch (option) {
+ .kitty_image_medium_file => screen.kitty_images.image_limits.file = val,
+ .kitty_image_medium_temp_file => screen.kitty_images.image_limits.temporary_file = val,
+ .kitty_image_medium_shared_mem => screen.kitty_images.image_limits.shared_memory = val,
+ else => unreachable,
+ }
+ }
+ },
+ .apc_max_bytes => {
+ wrapper.stream.handler.apc_handler.max_bytes = if (value) |ptr|
+ .initFull(ptr.*)
+ else
+ .{};
+ },
+ .apc_max_bytes_kitty => {
+ if (value) |ptr| {
+ wrapper.stream.handler.apc_handler.max_bytes.put(.kitty, ptr.*);
+ } else {
+ wrapper.stream.handler.apc_handler.max_bytes.remove(.kitty);
+ }
+ },
}
return .success;
}
@@ -400,21 +452,6 @@ pub const DeviceAttributes = Effects.CDeviceAttributes;
/// C: GhosttyTerminalScrollViewport
pub const ScrollViewport = ZigTerminal.ScrollViewport.C;
-/// C: GhosttyTerminalSelection
-pub const Selection = extern struct {
- start: point.Point.C,
- end: point.Point.C,
- rectangle: bool,
-
- fn toZig(self: Selection, terminal: *const ZigTerminal) ?selectionpkg {
- return selectionpkg.init(
- pinFromPoint(terminal, self.start) orelse return null,
- pinFromPoint(terminal, self.end) orelse return null,
- self.rectangle,
- );
- }
-};
-
pub fn scroll_viewport(
terminal_: Terminal,
behavior: ScrollViewport,
@@ -496,6 +533,9 @@ pub fn mode_set(
return .success;
}
+/// C: GhosttyKittyGraphics
+pub const KittyGraphics = kitty_gfx_c.KittyGraphics;
+
/// C: GhosttyTerminalScreen
pub const TerminalScreen = ScreenSet.Key;
@@ -530,6 +570,11 @@ pub const TerminalData = enum(c_int) {
color_background_default = 23,
color_cursor_default = 24,
color_palette_default = 25,
+ kitty_image_storage_limit = 26,
+ kitty_image_medium_file = 27,
+ kitty_image_medium_temp_file = 28,
+ kitty_image_medium_shared_mem = 29,
+ kitty_graphics = 30,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: TerminalData) type {
@@ -552,6 +597,12 @@ pub const TerminalData = enum(c_int) {
.color_cursor_default,
=> color.RGB.C,
.color_palette, .color_palette_default => color.PaletteC,
+ .kitty_image_storage_limit => u64,
+ .kitty_image_medium_file,
+ .kitty_image_medium_temp_file,
+ .kitty_image_medium_shared_mem,
+ => bool,
+ .kitty_graphics => KittyGraphics,
};
}
};
@@ -578,6 +629,27 @@ pub fn get(
};
}
+pub fn get_multi(
+ terminal_: Terminal,
+ count: usize,
+ keys: ?[*]const TerminalData,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = get(terminal_, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
fn getTyped(
terminal_: Terminal,
comptime data: TerminalData,
@@ -620,63 +692,60 @@ fn getTyped(
.color_cursor_default => out.* = (t.colors.cursor.default orelse return .no_value).cval(),
.color_palette => out.* = color.paletteCval(&t.colors.palette.current),
.color_palette_default => out.* = color.paletteCval(&t.colors.palette.original),
+ .kitty_image_storage_limit => {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+ out.* = @intCast(t.screens.active.kitty_images.total_limit);
+ },
+ .kitty_image_medium_file => {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+ out.* = t.screens.active.kitty_images.image_limits.file;
+ },
+ .kitty_image_medium_temp_file => {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+ out.* = t.screens.active.kitty_images.image_limits.temporary_file;
+ },
+ .kitty_image_medium_shared_mem => {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+ out.* = t.screens.active.kitty_images.image_limits.shared_memory;
+ },
+ .kitty_graphics => {
+ if (comptime !build_options.kitty_graphics) return .no_value;
+ out.* = &t.screens.active.kitty_images;
+ },
}
return .success;
}
-fn pointToZig(pt: point.Point.C) point.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 },
- };
-}
-
-fn pinFromPoint(terminal: *const ZigTerminal, pt: point.Point.C) ?PageList.Pin {
- return terminal.screens.active.pages.pin(pointToZig(pt));
-}
-
pub fn grid_ref(
terminal_: Terminal,
pt: point.Point.C,
out_ref: ?*grid_ref_c.CGridRef,
) callconv(lib.calling_conv) Result {
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
- const p = pinFromPoint(t, pt) orelse return .invalid_value;
+ 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 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 read_text(
+pub fn point_from_grid_ref(
terminal_: Terminal,
- selection: Selection,
- alloc_: ?*const CAllocator,
- out_ptr: *?[*]u8,
- out_len: *usize,
+ ref: *const grid_ref_c.CGridRef,
+ tag: point.Tag,
+ out: ?*point.Coordinate,
) callconv(lib.calling_conv) Result {
- out_ptr.* = null;
- out_len.* = 0;
-
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
- const alloc = lib.alloc.default(alloc_);
- const sel = selection.toZig(t) orelse return .invalid_value;
-
- var formatter: formatterpkg.ScreenFormatter = .init(t.screens.active, .{
- .emit = .plain,
- .unwrap = true,
- .trim = true,
- });
- formatter.content = .{ .selection = sel };
-
- var aw: std.Io.Writer.Allocating = .init(alloc);
- defer aw.deinit();
- formatter.format(&aw.writer) catch return .out_of_memory;
-
- const buf = aw.toOwnedSlice() catch return .out_of_memory;
- out_ptr.* = buf.ptr;
- out_len.* = buf.len;
+ const p = ref.toPin() orelse return .invalid_value;
+ const pt = t.screens.active.pages.pointFromPin(tag, p) orelse
+ return .no_value;
+ if (out) |o| o.* = pt.coord();
return .success;
}
@@ -983,6 +1052,29 @@ test "vt_write split escape sequence" {
try testing.expectEqualStrings("Hello Bold", str);
}
+test "vt_write split combining mark after base at right edge" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{
+ .cols = 2,
+ .rows = 2,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(t);
+
+ // Put "å" in the final column, then send its combining low line in a
+ // separate write so the mark arrives while the cursor has a pending wrap.
+ vt_write(t, "xå", 3);
+ vt_write(t, "\xcc\xb2", 2);
+
+ const str = try t.?.terminal.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("xå̲", str);
+}
+
test "get cols and rows" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
@@ -1244,6 +1336,102 @@ test "grid_ref null terminal" {
}, &out_ref));
}
+test "point_from_grid_ref roundtrip active" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer free(t);
+
+ vt_write(t, "Hello", 5);
+
+ // Get a grid ref at (2, 0) in active coords
+ var ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 2, .y = 0 } },
+ }, &ref));
+
+ // Convert back to active coords
+ var coord: point.Coordinate = undefined;
+ try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .active, &coord));
+ try testing.expectEqual(@as(size.CellCountInt, 2), coord.x);
+ try testing.expectEqual(@as(u32, 0), coord.y);
+}
+
+test "point_from_grid_ref roundtrip viewport" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer free(t);
+
+ vt_write(t, "Hello", 5);
+
+ var ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .viewport,
+ .value = .{ .viewport = .{ .x = 0, .y = 0 } },
+ }, &ref));
+
+ var coord: point.Coordinate = undefined;
+ try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .viewport, &coord));
+ try testing.expectEqual(@as(size.CellCountInt, 0), coord.x);
+ try testing.expectEqual(@as(u32, 0), coord.y);
+}
+
+test "point_from_grid_ref history ref to active returns no_value" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 4, .max_scrollback = 10_000 },
+ ));
+ defer free(t);
+
+ // Write enough lines to push content into scrollback
+ for (0..10) |_| {
+ vt_write(t, "line\n", 5);
+ }
+
+ // Get a ref to the first line (now in scrollback)
+ var ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .screen,
+ .value = .{ .screen = .{ .x = 0, .y = 0 } },
+ }, &ref));
+
+ // Should succeed for screen coords
+ var coord: point.Coordinate = undefined;
+ try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .screen, &coord));
+ try testing.expectEqual(@as(u32, 0), coord.y);
+
+ // Should fail for active coords (it's in scrollback)
+ try testing.expectEqual(Result.no_value, point_from_grid_ref(t, &ref, .active, &coord));
+}
+
+test "point_from_grid_ref null terminal" {
+ var ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.invalid_value, point_from_grid_ref(null, &ref, .active, null));
+}
+
+test "point_from_grid_ref null node" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 0 },
+ ));
+ defer free(t);
+
+ const ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.invalid_value, point_from_grid_ref(t, &ref, .active, null));
+}
+
test "set write_pty callback" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
@@ -2410,3 +2598,49 @@ test "set color sets dirty flag" {
try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
try testing.expect(zt.flags.dirty.palette);
}
+
+test "get_multi success" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 0 },
+ ));
+ defer free(t);
+
+ var cols: u16 = 0;
+ var rows: u16 = 0;
+ var written: usize = 0;
+
+ const keys = [_]TerminalData{ .cols, .rows };
+ var values = [_]?*anyopaque{ @ptrCast(&cols), @ptrCast(&rows) };
+ try testing.expectEqual(Result.success, get_multi(t, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+ try testing.expectEqual(80, cols);
+ try testing.expectEqual(24, rows);
+}
+
+test "get_multi error sets out_written" {
+ var t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 0 },
+ ));
+ defer free(t);
+
+ var cols: u16 = 0;
+ var written: usize = 99;
+
+ const keys = [_]TerminalData{ .cols, .invalid };
+ var values = [_]?*anyopaque{ @ptrCast(&cols), @ptrCast(&cols) };
+ try testing.expectEqual(Result.invalid_value, get_multi(t, keys.len, &keys, &values, &written));
+ try testing.expectEqual(1, written);
+ try testing.expectEqual(80, cols);
+}
+
+test "get_multi null keys returns invalid_value" {
+ var cols: u16 = 0;
+ var values = [_]?*anyopaque{@ptrCast(&cols)};
+ try testing.expectEqual(Result.invalid_value, get_multi(null, 1, null, &values, null));
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/types.zig b/vendor/libghostty-vt/src/terminal/c/types.zig
index 51184b47..500809d9 100644
--- a/vendor/libghostty-vt/src/terminal/c/types.zig
+++ b/vendor/libghostty-vt/src/terminal/c/types.zig
@@ -13,6 +13,7 @@ const size_report = @import("size_report.zig");
const terminal = @import("terminal.zig");
const formatter = @import("formatter.zig");
+const selection = @import("selection.zig");
const render = @import("render.zig");
const style_c = @import("style.zig");
const mouse_encode = @import("mouse_encode.zig");
@@ -26,6 +27,7 @@ pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
.{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
.{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
.{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
+ .{ "GhosttySelection", StructInfo.init(selection.CSelection) },
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
.{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
@@ -41,7 +43,6 @@ pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
.{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
.{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
.{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
- .{ "GhosttyTerminalSelection", StructInfo.init(terminal.Selection) },
});
/// The comptime-generated JSON string of all structs.
diff --git a/vendor/libghostty-vt/src/terminal/kitty/graphics.zig b/vendor/libghostty-vt/src/terminal/kitty/graphics.zig
index c710f81a..6659cd31 100644
--- a/vendor/libghostty-vt/src/terminal/kitty/graphics.zig
+++ b/vendor/libghostty-vt/src/terminal/kitty/graphics.zig
@@ -25,6 +25,7 @@ pub const unicode = @import("graphics_unicode.zig");
pub const Command = command.Command;
pub const CommandParser = command.Parser;
pub const Image = image.Image;
+pub const LoadingImage = image.LoadingImage;
pub const ImageStorage = storage.ImageStorage;
pub const RenderPlacement = render.Placement;
pub const Response = command.Response;
diff --git a/vendor/libghostty-vt/src/terminal/kitty/graphics_command.zig b/vendor/libghostty-vt/src/terminal/kitty/graphics_command.zig
index dfce56e3..aa35fbeb 100644
--- a/vendor/libghostty-vt/src/terminal/kitty/graphics_command.zig
+++ b/vendor/libghostty-vt/src/terminal/kitty/graphics_command.zig
@@ -3,6 +3,7 @@ const assert = @import("../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const simd = @import("../../simd/main.zig");
+const lib = @import("../lib.zig");
const log = std.log.scoped(.kitty_gfx);
@@ -37,6 +38,10 @@ pub const Parser = struct {
/// here instead of a fixed buffer so that we can too.
data: std.ArrayList(u8),
+ /// Maximum bytes the data payload can take. This is to prevent
+ /// malicious input from causing us to allocate too much memory.
+ max_bytes: usize,
+
/// Internal state for parsing.
state: State,
@@ -54,7 +59,7 @@ pub const Parser = struct {
/// Initialize the parser. The allocator given will be used for both
/// temporary data and long-lived values such as the final image blob.
- pub fn init(alloc: Allocator) Parser {
+ pub fn init(alloc: Allocator, max_bytes: usize) Parser {
var arena = ArenaAllocator.init(alloc);
errdefer arena.deinit();
var result: Parser = .{
@@ -63,6 +68,7 @@ pub const Parser = struct {
.kv = .{},
.kv_temp_len = 0,
.kv_current = 0,
+ .max_bytes = max_bytes,
.state = .control_key,
.kv_temp = undefined,
@@ -83,7 +89,7 @@ pub const Parser = struct {
/// Parse a complete command string.
pub fn parseString(alloc: Allocator, data: []const u8) !Command {
- var parser = init(alloc);
+ var parser = init(alloc, 1024 * 1024);
defer parser.deinit();
for (data) |c| try parser.feed(c);
return try parser.complete(alloc);
@@ -136,7 +142,10 @@ pub const Parser = struct {
else => {},
},
- .data => try self.data.append(self.arena.child_allocator, c),
+ .data => {
+ if (self.data.items.len >= self.max_bytes) return error.OutOfMemory;
+ try self.data.append(self.arena.child_allocator, c);
+ },
}
}
@@ -394,39 +403,38 @@ pub const Transmission = struct {
compression: Compression = .none, // o
more_chunks: bool = false, // m
- pub const Format = enum {
- rgb, // 24
- rgba, // 32
- png, // 100
-
+ pub const Format = lib.Enum(lib.target, &.{
+ "rgb", // 24
+ "rgba", // 32
+ "png", // 100
// The following are not supported directly via the protocol
// but they are formats that a png may decode to that we
// support.
- gray_alpha,
- gray,
-
- pub fn bpp(self: Format) u8 {
- return switch (self) {
- .gray => 1,
- .gray_alpha => 2,
- .rgb => 3,
- .rgba => 4,
- .png => unreachable, // Must be validated before
- };
- }
- };
-
- pub const Medium = enum {
- direct, // d
- file, // f
- temporary_file, // t
- shared_memory, // s
- };
-
- pub const Compression = enum {
- none,
- zlib_deflate, // z
- };
+ "gray_alpha",
+ "gray",
+ });
+
+ pub const Medium = lib.Enum(lib.target, &.{
+ "direct", // d
+ "file", // f
+ "temporary_file", // t
+ "shared_memory", // s
+ });
+
+ pub const Compression = lib.Enum(lib.target, &.{
+ "none",
+ "zlib_deflate", // z
+ });
+
+ pub fn formatBpp(format: Format) u8 {
+ return switch (format) {
+ .gray => 1,
+ .gray_alpha => 2,
+ .rgb => 3,
+ .rgba => 4,
+ .png => unreachable, // Must be validated before
+ };
+ }
fn parse(kv: KV) !Transmission {
var result: Transmission = .{};
@@ -964,7 +972,7 @@ pub const CompositionMode = enum {
test "transmission command" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "f=24,s=10,v=20";
@@ -982,7 +990,7 @@ test "transmission command" {
test "transmission ignores 'm' if medium is not direct" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=t,t=t,m=1";
@@ -999,7 +1007,7 @@ test "transmission ignores 'm' if medium is not direct" {
test "transmission respects 'm' if medium is direct" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=t,t=d,m=1";
@@ -1016,7 +1024,7 @@ test "transmission respects 'm' if medium is direct" {
test "query command" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "i=31,s=1,v=1,a=q,t=d,f=24;QUFBQQ";
@@ -1036,7 +1044,7 @@ test "query command" {
test "display command" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=p,U=1,i=31,c=80,r=120";
@@ -1054,7 +1062,7 @@ test "display command" {
test "delete command" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=d,d=p,x=3,y=4";
@@ -1074,7 +1082,7 @@ test "delete command" {
test "no control data" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = ";QUFBQQ";
@@ -1089,7 +1097,7 @@ test "no control data" {
test "ignore unknown keys (long)" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "f=24,s=10,v=20,hello=world";
@@ -1107,7 +1115,7 @@ test "ignore unknown keys (long)" {
test "ignore very long values" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "f=24,s=10,v=2000000000000000000000000000000000000000";
@@ -1125,7 +1133,7 @@ test "ignore very long values" {
test "ensure very large negative values don't get skipped" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=p,i=1,z=-2000000000";
@@ -1142,7 +1150,7 @@ test "ensure very large negative values don't get skipped" {
test "ensure proper overflow error for u32" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=p,i=10000000000";
@@ -1153,7 +1161,7 @@ test "ensure proper overflow error for u32" {
test "ensure proper overflow error for i32" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=p,i=1,z=-9999999999";
@@ -1167,7 +1175,7 @@ test "all i32 values" {
{
// 'z' (usually z-axis values)
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=p,i=1,z=-1";
for (input) |c| try p.feed(c);
@@ -1182,7 +1190,7 @@ test "all i32 values" {
{
// 'H' (relative placement, horizontal offset)
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=p,i=1,H=-1";
for (input) |c| try p.feed(c);
@@ -1197,7 +1205,7 @@ test "all i32 values" {
{
// 'V' (relative placement, vertical offset)
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=p,i=1,V=-1";
for (input) |c| try p.feed(c);
@@ -1254,7 +1262,7 @@ test "response: encode with image ID and number" {
test "delete range command 1" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=d,d=r,x=3,y=4";
@@ -1274,7 +1282,7 @@ test "delete range command 1" {
test "delete range command 2" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=d,d=R,x=5,y=11";
@@ -1294,7 +1302,7 @@ test "delete range command 2" {
test "delete range command 3" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=d,d=R,x=5,y=4";
@@ -1305,7 +1313,7 @@ test "delete range command 3" {
test "delete range command 4" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=d,d=R,x=5";
@@ -1316,7 +1324,7 @@ test "delete range command 4" {
test "delete range command 5" {
const testing = std.testing;
const alloc = testing.allocator;
- var p = Parser.init(alloc);
+ var p = Parser.init(alloc, 1024 * 1024);
defer p.deinit();
const input = "a=d,d=R,y=5";
diff --git a/vendor/libghostty-vt/src/terminal/kitty/graphics_exec.zig b/vendor/libghostty-vt/src/terminal/kitty/graphics_exec.zig
index 5b3ab915..a6a879e5 100644
--- a/vendor/libghostty-vt/src/terminal/kitty/graphics_exec.zig
+++ b/vendor/libghostty-vt/src/terminal/kitty/graphics_exec.zig
@@ -44,7 +44,7 @@ pub fn execute(
var quiet = cmd.quiet;
const resp_: ?Response = switch (cmd.control) {
- .query => query(alloc, cmd),
+ .query => query(alloc, terminal, cmd),
.display => display(alloc, terminal, cmd),
.delete => delete(alloc, terminal, cmd),
@@ -94,7 +94,11 @@ pub fn execute(
/// This command is used to attempt to load an image and respond with
/// success/error but does not persist any of the command to the terminal
/// state.
-fn query(alloc: Allocator, cmd: *const Command) Response {
+fn query(
+ alloc: Allocator,
+ terminal: *const Terminal,
+ cmd: *const Command,
+) Response {
const t = cmd.control.query;
// Query requires image ID. We can't actually send a response without
@@ -112,7 +116,8 @@ fn query(alloc: Allocator, cmd: *const Command) Response {
};
// Attempt to load the image. If we cannot, then set an appropriate error.
- var loading = LoadingImage.init(alloc, cmd) catch |err| {
+ const storage = &terminal.screens.active.kitty_images;
+ var loading = LoadingImage.init(alloc, cmd, storage.image_limits) catch |err| {
encodeError(&result, err);
return result;
};
@@ -322,7 +327,7 @@ fn loadAndAddImage(
}
break :loading loading.*;
- } else try .init(alloc, cmd);
+ } else try .init(alloc, cmd, storage.image_limits);
// We only want to deinit on error. If we're chunking, then we don't
// want to deinit at all. If we're not chunking, then we'll deinit
diff --git a/vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig b/vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig
index d2877cfc..8243a632 100644
--- a/vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig
+++ b/vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig
@@ -8,7 +8,7 @@ const posix = std.posix;
const fastmem = @import("../../fastmem.zig");
const command = @import("graphics_command.zig");
const PageList = @import("../PageList.zig");
-const wuffs = @import("wuffs");
+const sys = @import("../sys.zig");
const temp_dir = struct {
const TempDir = @import("../../os/TempDir.zig");
@@ -44,10 +44,38 @@ pub const LoadingImage = struct {
/// used if q isn't set on subsequent chunks.
quiet: command.Command.Quiet,
+ /// The limits of the Kitty Graphics protocol we should allow.
+ ///
+ /// This can be used to restrict the type of images and other
+ /// parameters for resource or security reasons. Note that depending
+ /// on how libghostty is compiled, some of these may be fully unsupported
+ /// and ignored (e.g. "file" on wasm32-freestanding).
+ pub const Limits = packed struct {
+ file: bool,
+ temporary_file: bool,
+ shared_memory: bool,
+
+ pub const all: Limits = .{
+ .file = true,
+ .temporary_file = true,
+ .shared_memory = true,
+ };
+
+ pub const direct: Limits = .{
+ .file = false,
+ .temporary_file = false,
+ .shared_memory = false,
+ };
+ };
+
/// Initialize a chunked immage from the first image transmission.
/// If this is a multi-chunk image, this should only be the FIRST
/// chunk.
- pub fn init(alloc: Allocator, cmd: *const command.Command) !LoadingImage {
+ pub fn init(
+ alloc: Allocator,
+ cmd: *const command.Command,
+ limits: Limits,
+ ) !LoadingImage {
// Build our initial image from the properties sent via the control.
// These can be overwritten by the data loading process. For example,
// PNG loading sets the width/height from the data.
@@ -72,6 +100,26 @@ pub const LoadingImage = struct {
return result;
}
+ // Verify our capabilities and limits allow this.
+ {
+ // Special case if we don't support decoding PNGs and the format
+ // is a PNG we can save a lot of memory/effort buffering the
+ // data but failing up front.
+ if (t.format == .png and
+ sys.decode_png == null)
+ {
+ return error.UnsupportedMedium;
+ }
+
+ // Verify the medium is allowed
+ switch (t.medium) {
+ .direct => unreachable,
+ .file => if (!limits.file) return error.UnsupportedMedium,
+ .temporary_file => if (!limits.temporary_file) return error.UnsupportedMedium,
+ .shared_memory => if (!limits.shared_memory) return error.UnsupportedMedium,
+ }
+ }
+
// Otherwise, the payload data is guaranteed to be a path.
if (comptime builtin.os.tag != .windows) {
@@ -111,8 +159,9 @@ pub const LoadingImage = struct {
t: command.Transmission,
path: []const u8,
) !void {
+ // android does not support POSIX shared memory.
// windows is currently unsupported, does it support shm?
- if (comptime builtin.target.os.tag == .windows) {
+ if (comptime builtin.abi.isAndroid() or builtin.target.os.tag == .windows) {
return error.UnsupportedMedium;
}
@@ -154,8 +203,8 @@ pub const LoadingImage = struct {
.png => stat_size,
// For these formats we have a size we must have.
- .gray, .gray_alpha, .rgb, .rgba => |f| size: {
- const bpp = f.bpp();
+ .gray, .gray_alpha, .rgb, .rgba => size: {
+ const bpp = command.Transmission.formatBpp(self.image.format);
break :size self.image.width * self.image.height * bpp;
},
};
@@ -281,17 +330,16 @@ pub const LoadingImage = struct {
fn isPathInTempDir(path: []const u8) bool {
if (std.mem.startsWith(u8, path, "/tmp")) return true;
if (std.mem.startsWith(u8, path, "/dev/shm")) return true;
- if (temp_dir.allocTmpDir(std.heap.page_allocator)) |dir| {
- defer temp_dir.freeTmpDir(std.heap.page_allocator, dir);
- if (std.mem.startsWith(u8, path, dir)) return true;
-
- // The temporary dir is sometimes a symlink. On macOS for
- // example /tmp is /private/var/...
- var buf: [std.fs.max_path_bytes]u8 = undefined;
- if (posix.realpath(dir, &buf)) |real_dir| {
- if (std.mem.startsWith(u8, path, real_dir)) return true;
- } else |_| {}
- }
+ const dir = temp_dir.allocTmpDir(std.heap.page_allocator) catch return false;
+ defer temp_dir.freeTmpDir(std.heap.page_allocator, dir);
+ if (std.mem.startsWith(u8, path, dir)) return true;
+
+ // The temporary dir is sometimes a symlink. On macOS for
+ // example /tmp is /private/var/...
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ if (posix.realpath(dir, &buf)) |real_dir| {
+ if (std.mem.startsWith(u8, path, real_dir)) return true;
+ } else |_| {}
return false;
}
@@ -342,7 +390,7 @@ pub const LoadingImage = struct {
if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge;
// Data length must be what we expect
- const bpp = img.format.bpp();
+ const bpp = command.Transmission.formatBpp(img.format);
const expected_len = img.width * img.height * bpp;
const actual_len = self.data.items.len;
if (actual_len != expected_len) {
@@ -426,13 +474,14 @@ pub const LoadingImage = struct {
fn decodePng(self: *LoadingImage, alloc: Allocator) !void {
assert(self.image.format == .png);
- const result = wuffs.png.decode(
+ const decode_png_fn = sys.decode_png orelse
+ return error.UnsupportedFormat;
+ const result = decode_png_fn(
alloc,
self.data.items,
) catch |err| switch (err) {
- error.WuffsError => return error.InvalidData,
+ error.InvalidData => return error.InvalidData,
error.OutOfMemory => return error.OutOfMemory,
- error.Overflow => return error.InvalidData,
};
defer alloc.free(result.data);
@@ -522,7 +571,7 @@ test "image load with invalid RGB data" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
}
@@ -540,7 +589,7 @@ test "image load with image too wide" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc));
}
@@ -559,7 +608,7 @@ test "image load with image too tall" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc));
}
@@ -583,7 +632,7 @@ test "image load: rgb, zlib compressed, direct" {
),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -611,7 +660,7 @@ test "image load: rgb, not compressed, direct" {
),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -640,7 +689,7 @@ test "image load: rgb, zlib compressed, direct, chunked" {
.data = try alloc.dupe(u8, data[0..1024]),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
// Read our remaining chunks
@@ -676,7 +725,7 @@ test "image load: rgb, zlib compressed, direct, chunked with zero initial chunk"
} },
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
// Read our remaining chunks
@@ -720,7 +769,7 @@ test "image load: temporary file without correct path" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
- try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd));
+ try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd, .all));
// Temporary file should still be there
try tmp_dir.dir.access(path, .{});
@@ -753,7 +802,7 @@ test "image load: rgb, not compressed, temporary file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -790,7 +839,7 @@ test "image load: rgb, not compressed, regular file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -799,6 +848,8 @@ test "image load: rgb, not compressed, regular file" {
}
test "image load: png, not compressed, regular file" {
+ if (sys.decode_png == null) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
@@ -825,7 +876,7 @@ test "image load: png, not compressed, regular file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
- var loading = try LoadingImage.init(alloc, &cmd);
+ var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -833,3 +884,161 @@ test "image load: png, not compressed, regular file" {
try testing.expect(img.format == .rgba);
try tmp_dir.dir.access(path, .{});
}
+
+test "limits: direct medium always allowed" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var cmd: command.Command = .{
+ .control = .{ .transmit = .{
+ .format = .rgb,
+ .medium = .direct,
+ .width = 1,
+ .height = 1,
+ .image_id = 31,
+ } },
+ .data = try alloc.dupe(u8, "AAAA"),
+ };
+ defer cmd.deinit(alloc);
+
+ // Direct medium should work even with the most restrictive limits
+ var loading = try LoadingImage.init(alloc, &cmd, .direct);
+ defer loading.deinit(alloc);
+}
+
+test "limits: file medium blocked by limits" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var tmp_dir = try temp_dir.TempDir.init();
+ defer tmp_dir.deinit();
+ const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
+ try tmp_dir.dir.writeFile(.{
+ .sub_path = "image.data",
+ .data = data,
+ });
+
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try tmp_dir.dir.realpath("image.data", &buf);
+
+ var cmd: command.Command = .{
+ .control = .{ .transmit = .{
+ .format = .rgb,
+ .medium = .file,
+ .compression = .none,
+ .width = 20,
+ .height = 15,
+ .image_id = 31,
+ } },
+ .data = try alloc.dupe(u8, path),
+ };
+ defer cmd.deinit(alloc);
+ try testing.expectError(error.UnsupportedMedium, LoadingImage.init(alloc, &cmd, .direct));
+}
+
+test "limits: file medium allowed by limits" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var tmp_dir = try temp_dir.TempDir.init();
+ defer tmp_dir.deinit();
+ const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
+ try tmp_dir.dir.writeFile(.{
+ .sub_path = "image.data",
+ .data = data,
+ });
+
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try tmp_dir.dir.realpath("image.data", &buf);
+
+ var cmd: command.Command = .{
+ .control = .{ .transmit = .{
+ .format = .rgb,
+ .medium = .file,
+ .compression = .none,
+ .width = 20,
+ .height = 15,
+ .image_id = 31,
+ } },
+ .data = try alloc.dupe(u8, path),
+ };
+ defer cmd.deinit(alloc);
+ var loading = try LoadingImage.init(alloc, &cmd, .{
+ .file = true,
+ .temporary_file = false,
+ .shared_memory = false,
+ });
+ defer loading.deinit(alloc);
+}
+
+test "limits: temporary file medium blocked by limits" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var tmp_dir = try temp_dir.TempDir.init();
+ defer tmp_dir.deinit();
+ const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
+ try tmp_dir.dir.writeFile(.{
+ .sub_path = "tty-graphics-protocol-image.data",
+ .data = data,
+ });
+
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf);
+
+ var cmd: command.Command = .{
+ .control = .{ .transmit = .{
+ .format = .rgb,
+ .medium = .temporary_file,
+ .compression = .none,
+ .width = 20,
+ .height = 15,
+ .image_id = 31,
+ } },
+ .data = try alloc.dupe(u8, path),
+ };
+ defer cmd.deinit(alloc);
+ try testing.expectError(error.UnsupportedMedium, LoadingImage.init(alloc, &cmd, .{
+ .file = true,
+ .temporary_file = false,
+ .shared_memory = true,
+ }));
+
+ // File should still exist since we blocked before reading
+ try tmp_dir.dir.access("tty-graphics-protocol-image.data", .{});
+}
+
+test "limits: temporary file medium allowed by limits" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var tmp_dir = try temp_dir.TempDir.init();
+ defer tmp_dir.deinit();
+ const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
+ try tmp_dir.dir.writeFile(.{
+ .sub_path = "tty-graphics-protocol-image.data",
+ .data = data,
+ });
+
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf);
+
+ var cmd: command.Command = .{
+ .control = .{ .transmit = .{
+ .format = .rgb,
+ .medium = .temporary_file,
+ .compression = .none,
+ .width = 20,
+ .height = 15,
+ .image_id = 31,
+ } },
+ .data = try alloc.dupe(u8, path),
+ };
+ defer cmd.deinit(alloc);
+ var loading = try LoadingImage.init(alloc, &cmd, .{
+ .file = false,
+ .temporary_file = true,
+ .shared_memory = false,
+ });
+ defer loading.deinit(alloc);
+}
diff --git a/vendor/libghostty-vt/src/terminal/kitty/graphics_storage.zig b/vendor/libghostty-vt/src/terminal/kitty/graphics_storage.zig
index 8ff68e3f..e017d5f7 100644
--- a/vendor/libghostty-vt/src/terminal/kitty/graphics_storage.zig
+++ b/vendor/libghostty-vt/src/terminal/kitty/graphics_storage.zig
@@ -51,6 +51,9 @@ pub const ImageStorage = struct {
/// Non-null if there is an in-progress loading image.
loading: ?*LoadingImage = null,
+ /// The limits of what medium types are allowed for image loading.
+ image_limits: LoadingImage.Limits = .direct,
+
/// The total bytes of image data that have been loaded and the limit.
/// If the limit is reached, the oldest images will be evicted to make
/// space. Unused images take priority.
@@ -89,8 +92,9 @@ pub const ImageStorage = struct {
) !void {
// Special case disabling by quickly deleting all
if (limit == 0) {
+ const image_limits = self.image_limits;
self.deinit(alloc, s);
- self.* = .{};
+ self.* = .{ .image_limits = image_limits };
}
// If we re lowering our limit, check if we need to evict.
@@ -658,9 +662,10 @@ pub const ImageStorage = struct {
}
}
- /// Calculates the size of this placement's image in pixels,
- /// taking in to account the specified rows and columns.
- pub fn calculatedSize(
+ /// Returns the size of this placement's image in pixels,
+ /// taking into account the source rectangle, specified
+ /// rows/columns, and aspect ratio.
+ pub fn pixelSize(
self: Placement,
image: Image,
t: *const terminal.Terminal,
@@ -755,7 +760,7 @@ pub const ImageStorage = struct {
// Otherwise we calculate the pixel size, divide by
// cell size, and round up to the nearest integer.
- const calc_size = self.calculatedSize(image, t);
+ const calc_size = self.pixelSize(image, t);
return .{
.cols = std.math.divCeil(
u32,
@@ -1334,7 +1339,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" {
// that's 100px width. 100px * (9 / 16) = 56.25, which should round
// to a height of 56px.
- const calc_size = placement.calculatedSize(image, &t);
+ const calc_size = placement.pixelSize(image, &t);
try testing.expectEqual(@as(u32, 100), calc_size.width);
try testing.expectEqual(@as(u32, 56), calc_size.height);
}
@@ -1352,7 +1357,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" {
// 100px height. 100px * (16 / 9) = 177.77..., which should round to
// a width of 178px.
- const calc_size = placement.calculatedSize(image, &t);
+ const calc_size = placement.pixelSize(image, &t);
try testing.expectEqual(@as(u32, 178), calc_size.width);
try testing.expectEqual(@as(u32, 100), calc_size.height);
}
diff --git a/vendor/libghostty-vt/src/terminal/main.zig b/vendor/libghostty-vt/src/terminal/main.zig
index 9f5b65e3..87a9aded 100644
--- a/vendor/libghostty-vt/src/terminal/main.zig
+++ b/vendor/libghostty-vt/src/terminal/main.zig
@@ -23,6 +23,7 @@ pub const search = @import("search.zig");
pub const sgr = @import("sgr.zig");
pub const size = @import("size.zig");
pub const size_report = @import("size_report.zig");
+pub const sys = @import("sys.zig");
pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {};
pub const x11_color = @import("x11_color.zig");
diff --git a/vendor/libghostty-vt/src/terminal/modes.zig b/vendor/libghostty-vt/src/terminal/modes.zig
index 0d8cde5b..c92f97b0 100644
--- a/vendor/libghostty-vt/src/terminal/modes.zig
+++ b/vendor/libghostty-vt/src/terminal/modes.zig
@@ -270,6 +270,11 @@ const entries: []const ModeEntry = &.{
.{ .name = "reverse_wrap", .value = 45 },
.{ .name = "alt_screen_legacy", .value = 47 },
.{ .name = "keypad_keys", .value = 66 },
+ // DEC Backarrow Key Mode (DECBKM)
+ // See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170
+ // If `false` (the default), `backspace` emits 0x7f
+ // If `true`, `backspace` emits 0x08
+ .{ .name = "backarrow_key_mode", .value = 67 },
.{ .name = "enable_left_and_right_margin", .value = 69 },
.{ .name = "mouse_event_normal", .value = 1000 },
.{ .name = "mouse_event_button", .value = 1002 },
diff --git a/vendor/libghostty-vt/src/terminal/stream_terminal.zig b/vendor/libghostty-vt/src/terminal/stream_terminal.zig
index 8e0f9111..f68f088b 100644
--- a/vendor/libghostty-vt/src/terminal/stream_terminal.zig
+++ b/vendor/libghostty-vt/src/terminal/stream_terminal.zig
@@ -1,5 +1,7 @@
const std = @import("std");
+const build_options = @import("terminal_options");
const testing = std.testing;
+const apc = @import("apc.zig");
const csi = @import("csi.zig");
const device_attributes = @import("device_attributes.zig");
const device_status = @import("device_status.zig");
@@ -35,6 +37,12 @@ pub const Handler = struct {
/// effects.
effects: Effects = .readonly,
+ /// The APC command handler maintains the APC state. APC is like
+ /// CSI or OSC, but it is a private escape sequence that is used
+ /// to send commands to the terminal emulator. This is used by
+ /// the kitty graphics protocol.
+ apc_handler: apc.Handler = .{},
+
pub const Effects = struct {
/// Called when the terminal needs to write data back to the pty,
/// e.g. in response to a DECRQM query. The data is only valid
@@ -98,9 +106,7 @@ pub const Handler = struct {
}
pub fn deinit(self: *Handler) void {
- // Currently does nothing but may in the future so callers should
- // call this.
- _ = self;
+ self.apc_handler.deinit();
}
pub fn vt(
@@ -230,6 +236,11 @@ pub const Handler = struct {
.color_operation => try self.colorOperation(value.op, &value.requests),
.kitty_color_report => try self.kittyColorOperation(value),
+ // APC
+ .apc_start => self.apc_handler.start(),
+ .apc_put => self.apc_handler.feed(self.terminal.gpa(), value),
+ .apc_end => self.apcEnd(),
+
// Effect-based handlers
.bell => self.bell(),
.device_attributes => self.reportDeviceAttributes(value),
@@ -249,13 +260,6 @@ pub const Handler = struct {
.dcs_unhook,
=> {},
- // APC can modify terminal state (Kitty graphics) but we don't
- // currently support it in the readonly stream.
- .apc_start,
- .apc_end,
- .apc_put,
- => {},
-
// Have no terminal-modifying effect
.report_pwd,
.show_desktop_notification,
@@ -650,6 +654,33 @@ pub const Handler = struct {
}
}
}
+
+ fn apcEnd(self: *Handler) void {
+ const alloc = self.terminal.gpa();
+ var cmd = self.apc_handler.end() orelse return;
+ defer cmd.deinit(alloc);
+
+ switch (cmd) {
+ .kitty => |*kitty_cmd| if (comptime build_options.kitty_graphics) {
+ if (self.terminal.kittyGraphics(
+ alloc,
+ kitty_cmd,
+ )) |resp| resp: {
+ // Don't waste time encoding if we can't write responses
+ // anyways.
+ if (self.effects.write_pty == null) break :resp;
+
+ // Encode and write the response if we have one.
+ var buf: [1024]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ resp.encode(&writer) catch return;
+ writer.writeByte(0) catch return;
+ const final = writer.buffered();
+ if (final.len > 3) self.writePty(final[0 .. final.len - 1 :0]);
+ }
+ },
+ }
+ }
};
test "basic print" {
@@ -2069,3 +2100,52 @@ test "device attributes: custom response" {
s.nextSlice("\x1B[>c");
try testing.expectEqualStrings("\x1b[>41;100;0c", S.written.?);
}
+
+test "kitty graphics APC response" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ defer t.deinit(testing.allocator);
+
+ const S = struct {
+ var written: ?[]const u8 = null;
+ fn writePty(_: *Handler, data: [:0]const u8) void {
+ if (written) |old| testing.allocator.free(old);
+ written = testing.allocator.dupe(u8, data) catch @panic("OOM");
+ }
+ };
+ S.written = null;
+ defer if (S.written) |old| testing.allocator.free(old);
+
+ var handler: Handler = .init(&t);
+ handler.effects.write_pty = &S.writePty;
+
+ var s: Stream = .initAlloc(testing.allocator, handler);
+ defer s.deinit();
+
+ // Send a kitty graphics transmit command with image id 1
+ s.nextSlice("\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2,c=10,r=1;////////\x1b\\");
+
+ // Should have written a response back
+ try testing.expectEqualStrings("\x1b_Gi=1;OK\x1b\\", S.written.?);
+}
+
+test "kitty graphics via APC" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ defer t.deinit(testing.allocator);
+
+ const handler: Handler = .init(&t);
+ var s: Stream = .initAlloc(testing.allocator, handler);
+ defer s.deinit();
+
+ // Send a kitty graphics transmit command via APC:
+ // ESC _ G <payload> ESC \
+ // a=t,t=d,f=24,i=1,s=1,v=2,c=10,r=1;//////// (1x2 RGB direct)
+ s.nextSlice("\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2,c=10,r=1;////////\x1b\\");
+
+ const storage = &t.screens.active.kitty_images;
+ const img = storage.imageById(1).?;
+ try testing.expectEqual(.rgb, img.format);
+}
diff --git a/vendor/libghostty-vt/src/terminal/sys.zig b/vendor/libghostty-vt/src/terminal/sys.zig
new file mode 100644
index 00000000..f0c64da5
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/sys.zig
@@ -0,0 +1,54 @@
+//! System interface for the terminal package.
+//!
+//! This provides runtime-swappable function pointers for operations that
+//! depend on external implementations (e.g. image decoding). Each function
+//! pointer is initialized with a default implementation if available.
+//!
+//! This exists so that the terminal package doesn't have hard dependencies
+//! on specific libraries and enables embedders of the terminal package to
+//! swap out implementations as needed at startup to provide their own
+//! implementations.
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const build_options = @import("terminal_options");
+
+/// Decode PNG data into RGBA pixels. If null, PNG decoding is unsupported
+/// and the exact semantics are up to callers. For example, the Kitty Graphics
+/// Protocol will work but cannot accept PNG images.
+pub var decode_png: ?DecodePngFn = png: {
+ if (build_options.artifact == .lib) break :png null;
+ break :png &decodePngWuffs;
+};
+
+pub const DecodeError = Allocator.Error || error{InvalidData};
+pub const DecodePngFn = *const fn (Allocator, []const u8) DecodeError!Image;
+
+/// The result of decoding an image. The caller owns the returned data
+/// and must free it with the same allocator that was passed to the
+/// decode function.
+pub const Image = struct {
+ width: u32,
+ height: u32,
+ data: []u8,
+};
+
+fn decodePngWuffs(
+ alloc: Allocator,
+ data: []const u8,
+) DecodeError!Image {
+ const wuffs = @import("wuffs");
+ const result = wuffs.png.decode(
+ alloc,
+ data,
+ ) catch |err| switch (err) {
+ error.WuffsError => return error.InvalidData,
+ error.OutOfMemory => return error.OutOfMemory,
+ error.Overflow => return error.InvalidData,
+ };
+
+ return .{
+ .width = result.width,
+ .height = result.height,
+ .data = result.data,
+ };
+}