diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:28:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:29:28 +0300 |
| commit | 4a3302d19a2baa3a33e910e0088c24e780905b90 (patch) | |
| tree | dcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/terminal/apc.zig | |
| parent | 550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (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.zig | 308 |
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); +} |
