aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/apc.zig
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:28:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:29:28 +0300
commit4a3302d19a2baa3a33e910e0088c24e780905b90 (patch)
treedcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/terminal/apc.zig
parent550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff)
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/apc.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/apc.zig308
1 files changed, 291 insertions, 17 deletions
diff --git a/vendor/libghostty-vt/src/terminal/apc.zig b/vendor/libghostty-vt/src/terminal/apc.zig
index 7e6f08a7..121f906a 100644
--- a/vendor/libghostty-vt/src/terminal/apc.zig
+++ b/vendor/libghostty-vt/src/terminal/apc.zig
@@ -2,6 +2,7 @@ const std = @import("std");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
+pub const glyph = @import("apc/glyph.zig");
const kitty_gfx = @import("kitty/graphics.zig");
const log = std.log.scoped(.terminal_apc);
@@ -18,15 +19,27 @@ pub const Handler = struct {
/// use `.initFull`.
max_bytes: std.EnumMap(Protocol, usize) = .initFullWith(.{
.kitty = Protocol.defaultMaxBytes(.kitty),
+ .glyph = Protocol.defaultMaxBytes(.glyph),
}),
+ /// Protocols recognized by this APC handler. When a protocol is absent,
+ /// matching APC sequences are ignored so callers see the same behavior as
+ /// an unsupported protocol: no command execution and no response.
+ enabled: std.EnumSet(Protocol) = .initFull(),
+
pub fn deinit(self: *Handler) void {
self.state.deinit();
}
pub fn start(self: *Handler) void {
self.state.deinit();
- self.state = .{ .identify = {} };
+ self.state = .{ .identify = .{} };
+ }
+
+ /// Enable or disable APC protocol recognition for future APC sequences.
+ /// This does not affect any APC command already being parsed.
+ pub fn enable(self: *Handler, protocol: Protocol, enabled: bool) void {
+ self.enabled.setPresent(protocol, enabled);
}
pub fn feed(self: *Handler, alloc: Allocator, byte: u8) void {
@@ -38,21 +51,50 @@ pub const Handler = struct {
.ignore => return,
// We identify the APC command by the first byte.
- .identify => {
- switch (byte) {
- // Kitty graphics protocol
- 'G' => self.state = if (comptime build_options.kitty_graphics)
- .{ .kitty = .init(
+ .identify => |*id| id: {
+ // Kitty graphics is detected immediately on the `G` byte,
+ // since commands begin immediately after with no termination
+ // character after the 'G'.
+ if (comptime build_options.kitty_graphics) {
+ if (id.len == 0 and
+ byte == 'G' and
+ self.enabled.contains(.kitty))
+ {
+ self.state = .{ .kitty = .init(
alloc,
self.max_bytes.get(.kitty) orelse
Protocol.defaultMaxBytes(.kitty),
- ) }
- else
- .ignore,
+ ) };
+ break :id;
+ }
+ }
- // Unknown
- else => self.state = .ignore,
+ // If we hit `;` then identify...
+ if (byte == ';') {
+ const str = id.buf[0..id.len];
+ if (std.mem.eql(u8, str, "25a1") and
+ self.enabled.contains(.glyph))
+ {
+ self.state = .{ .glyph = .init(
+ alloc,
+ self.max_bytes.get(.glyph) orelse
+ Protocol.defaultMaxBytes(.glyph),
+ ) };
+ } else {
+ self.state = .ignore;
+ }
+
+ break :id;
}
+
+ // If we're out of space to buffer then we're done.
+ if (id.len >= id.buf.len) {
+ self.state = .ignore;
+ break :id;
+ }
+
+ id.buf[id.len] = byte;
+ id.len += 1;
},
.kitty => |*p| if (comptime build_options.kitty_graphics) {
@@ -62,6 +104,53 @@ pub const Handler = struct {
self.state = .ignore;
};
} else unreachable,
+
+ .glyph => |*p| p.feed(byte) catch |err| {
+ log.warn("glyph protocol error: {}", .{err});
+ p.deinit();
+ self.state = .ignore;
+ },
+ }
+ }
+
+ /// Feed a slice of bytes to the handler. This is equivalent to
+ /// calling feed for each byte in order, but protocol payload bytes
+ /// are passed through in bulk so large payloads (e.g. Kitty graphics
+ /// images) avoid per-byte dispatch overhead.
+ pub fn feedSlice(self: *Handler, alloc: Allocator, bytes: []const u8) void {
+ var rem = bytes;
+ while (rem.len > 0) {
+ switch (self.state) {
+ .inactive => unreachable,
+
+ // We're ignoring this APC command; drop the whole slice.
+ .ignore => return,
+
+ // Identification consumes at most a few bytes; step
+ // through them one at a time until the state changes.
+ .identify => {
+ self.feed(alloc, rem[0]);
+ rem = rem[1..];
+ },
+
+ .kitty => |*p| if (comptime build_options.kitty_graphics) {
+ p.feedSlice(rem) catch |err| {
+ log.warn("kitty graphics protocol error: {}", .{err});
+ p.deinit();
+ self.state = .ignore;
+ };
+ return;
+ } else unreachable,
+
+ .glyph => |*p| {
+ p.feedSlice(rem) catch |err| {
+ log.warn("glyph protocol error: {}", .{err});
+ p.deinit();
+ self.state = .ignore;
+ };
+ return;
+ },
+ }
}
}
@@ -86,21 +175,40 @@ pub const Handler = struct {
break :kitty .{ .kitty = command };
},
+
+ .glyph => |*p| glyph_cmd: {
+ const command = p.complete(p.alloc) catch |err| {
+ log.warn("glyph protocol error: {}", .{err});
+ break :glyph_cmd null;
+ };
+
+ break :glyph_cmd .{ .glyph = command };
+ },
};
}
};
pub const State = union(enum) {
/// We're not in the middle of an APC command yet.
- inactive: void,
+ inactive,
/// We got an unrecognized APC sequence or the APC sequence we
/// recognized became invalid. We're just dropping bytes.
- ignore: void,
-
- /// We're waiting to identify the APC sequence. This is done by
- /// inspecting the first byte of the sequence.
- identify: void,
+ ignore,
+
+ /// We're waiting to identify the APC sequence. The way this is done
+ /// is pretty fluid depending on supported APC protocols, but for now
+ /// our rule is:
+ ///
+ /// * 'G' - immediate transition to Kitty graphics protocol
+ /// * Buffer up to `;` and the bytes before dictate the protocol.
+ /// If we overflow then we're immediately invalid because we don't
+ /// support anything longer than this.
+ ///
+ identify: struct {
+ len: u3 = 0,
+ buf: [4]u8 = undefined,
+ },
/// Kitty graphics protocol
kitty: if (build_options.kitty_graphics)
@@ -108,9 +216,13 @@ pub const State = union(enum) {
else
void,
+ /// Glyph protocol
+ glyph: glyph.CommandParser,
+
pub fn deinit(self: *State) void {
switch (self.*) {
.inactive, .ignore, .identify => {},
+ .glyph => |*v| v.deinit(),
.kitty => |*v| if (comptime build_options.kitty_graphics)
v.deinit()
else
@@ -122,6 +234,7 @@ pub const State = union(enum) {
/// Possible APC command types.
pub const Protocol = enum {
kitty,
+ glyph,
/// Returns the default maximum bytes for the given protocol.
pub fn defaultMaxBytes(self: Protocol) usize {
@@ -129,6 +242,10 @@ pub const Protocol = enum {
// 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,
+ // Glyph protocol messages carry single glyf outlines which
+ // are small, but base64 encoding inflates them. 1 MiB is
+ // generous for any single simple-glyph record.
+ .glyph => 1 * 1024 * 1024,
};
}
};
@@ -140,12 +257,16 @@ pub const Command = union(Protocol) {
else
void,
+ glyph: glyph.Request,
+
pub fn deinit(self: *Command, alloc: Allocator) void {
switch (self.*) {
.kitty => |*v| if (comptime build_options.kitty_graphics)
v.deinit(alloc)
else
unreachable,
+
+ .glyph => |*v| v.deinit(alloc),
}
}
};
@@ -246,3 +367,156 @@ test "valid Kitty command" {
defer cmd.deinit(alloc);
try testing.expect(cmd == .kitty);
}
+
+test "identify with unrecognized command" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("abcd;payload") |c| h.feed(alloc, c);
+ try testing.expect(h.end() == null);
+}
+
+test "identify buffer overflow" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("abcde;payload") |c| h.feed(alloc, c);
+ try testing.expect(h.end() == null);
+}
+
+test "identify with no input" {
+ const testing = std.testing;
+
+ var h: Handler = .{};
+ h.start();
+ try testing.expect(h.end() == null);
+}
+
+test "identify with unknown partial input" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("25a") |c| h.feed(alloc, c);
+ try testing.expect(h.end() == null);
+}
+
+test "garbage glyph command" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("25a1;X") |c| h.feed(alloc, c);
+
+ try testing.expect(h.end() == null);
+}
+
+test "valid glyph command" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("25a1;q;cp=E0A0") |c| h.feed(alloc, c);
+
+ var cmd = h.end().?;
+ defer cmd.deinit(alloc);
+ try testing.expect(cmd == .glyph);
+ try testing.expect(cmd.glyph == .query);
+}
+
+test "feedSlice valid Kitty command" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ h.feedSlice(alloc, "Gf=24,s=10,v=20;aGVsbG8=");
+
+ var cmd = h.end().?;
+ defer cmd.deinit(alloc);
+ try testing.expect(cmd == .kitty);
+
+ // The payload is base64-decoded by the parser on completion.
+ try testing.expectEqualStrings("hello", cmd.kitty.data);
+}
+
+test "feedSlice identify split across slices" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ h.feedSlice(alloc, "G");
+ h.feedSlice(alloc, "f=24,s=10,");
+ h.feedSlice(alloc, "v=20;aGVsbG8=");
+
+ var cmd = h.end().?;
+ defer cmd.deinit(alloc);
+ try testing.expect(cmd == .kitty);
+
+ // The payload is base64-decoded by the parser on completion.
+ try testing.expectEqualStrings("hello", cmd.kitty.data);
+}
+
+test "feedSlice unknown APC command is ignored" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ h.feedSlice(alloc, "Xabcdef1234");
+ try testing.expect(h.state == .ignore);
+ h.feedSlice(alloc, "more data that is dropped");
+ try testing.expect(h.end() == null);
+}
+
+test "feedSlice valid glyph command" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ h.feedSlice(alloc, "25a1;q;cp=E0A0");
+
+ var cmd = h.end().?;
+ defer cmd.deinit(alloc);
+ try testing.expect(cmd == .glyph);
+ try testing.expect(cmd.glyph == .query);
+}
+
+test "feedSlice 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();
+ h.feedSlice(alloc, "Ga=t;abcd");
+ try testing.expect(h.state != .ignore);
+ h.feedSlice(alloc, "e");
+ try testing.expect(h.state == .ignore);
+}
+
+test "disabled glyph command is ignored" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.enable(.glyph, false);
+ h.start();
+ for ("25a1;q;cp=e0a0") |c| h.feed(alloc, c);
+ try testing.expect(h.end() == null);
+}