aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/apc.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/apc.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/apc.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/apc.zig186
1 files changed, 186 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/apc.zig b/vendor/libghostty-vt/src/terminal/apc.zig
new file mode 100644
index 00000000..3ebacbbf
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/apc.zig
@@ -0,0 +1,186 @@
+const std = @import("std");
+const build_options = @import("terminal_options");
+const Allocator = std.mem.Allocator;
+
+const kitty_gfx = @import("kitty/graphics.zig");
+
+const log = std.log.scoped(.terminal_apc);
+
+/// APC command handler. This should be hooked into a terminal.Stream handler.
+/// 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 = {} },
+
+ pub fn deinit(self: *Handler) void {
+ self.state.deinit();
+ }
+
+ pub fn start(self: *Handler) void {
+ self.state.deinit();
+ self.state = .{ .identify = {} };
+ }
+
+ pub fn feed(self: *Handler, alloc: Allocator, byte: u8) void {
+ switch (self.state) {
+ .inactive => unreachable,
+
+ // We're ignoring this APC command, likely because we don't
+ // recognize it so there is no need to store the data in memory.
+ .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 = kitty_gfx.CommandParser.init(alloc) }
+ else
+ .{ .ignore = {} },
+
+ // Unknown
+ 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 = {} };
+ };
+ } else unreachable,
+ }
+ }
+
+ pub fn end(self: *Handler) ?Command {
+ defer {
+ self.state.deinit();
+ self.state = .{ .inactive = {} };
+ }
+
+ return switch (self.state) {
+ .inactive => unreachable,
+ .ignore, .identify => null,
+ .kitty => |*p| kitty: {
+ if (comptime !build_options.kitty_graphics) unreachable;
+
+ // Use the same allocator that was used to create the parser.
+ const alloc = p.arena.child_allocator;
+ const command = p.complete(alloc) catch |err| {
+ log.warn("kitty graphics protocol error: {}", .{err});
+ break :kitty null;
+ };
+
+ break :kitty .{ .kitty = command };
+ },
+ };
+ }
+};
+
+pub const State = union(enum) {
+ /// We're not in the middle of an APC command yet.
+ inactive: void,
+
+ /// 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,
+
+ /// Kitty graphics protocol
+ kitty: if (build_options.kitty_graphics)
+ kitty_gfx.CommandParser
+ else
+ void,
+
+ pub fn deinit(self: *State) void {
+ switch (self.*) {
+ .inactive, .ignore, .identify => {},
+ .kitty => |*v| if (comptime build_options.kitty_graphics)
+ v.deinit()
+ else
+ unreachable,
+ }
+ }
+};
+
+/// Possible APC commands.
+pub const Command = union(enum) {
+ kitty: if (build_options.kitty_graphics)
+ kitty_gfx.Command
+ else
+ void,
+
+ pub fn deinit(self: *Command, alloc: Allocator) void {
+ switch (self.*) {
+ .kitty => |*v| if (comptime build_options.kitty_graphics)
+ v.deinit(alloc)
+ else
+ unreachable,
+ }
+ }
+};
+
+test "unknown APC command" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("Xabcdef1234") |c| h.feed(alloc, c);
+ try testing.expect(h.end() == null);
+}
+
+test "garbage Kitty command" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("Gabcdef1234") |c| h.feed(alloc, c);
+ try testing.expect(h.end() == null);
+}
+
+test "Kitty command with overflow u32" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("Ga=p,i=10000000000") |c| h.feed(alloc, c);
+ try testing.expect(h.end() == null);
+}
+
+test "Kitty command with overflow i32" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{};
+ h.start();
+ for ("Ga=p,i=1,z=-9999999999") |c| h.feed(alloc, c);
+ try testing.expect(h.end() == null);
+}
+
+test "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();
+ const input = "Gf=24,s=10,v=20,hello=world";
+ for (input) |c| h.feed(alloc, c);
+
+ var cmd = h.end().?;
+ defer cmd.deinit(alloc);
+ try testing.expect(cmd == .kitty);
+}