diff --git a/src/terminal/apc.zig b/src/terminal/apc.zig new file mode 100644 index 000000000..6a6b8cc36 --- /dev/null +++ b/src/terminal/apc.zig @@ -0,0 +1,137 @@ +const std = @import("std"); +const assert = std.debug.assert; +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 = .{ .kitty = kitty_gfx.CommandParser.init(alloc) }, + + // Unknown + else => self.state = .{ .ignore = {} }, + } + }, + + .kitty => |*p| p.feed(byte) catch |err| { + log.warn("kitty graphics protocol error: {}", .{err}); + self.state = .{ .ignore = {} }; + }, + } + } + + 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: { + const command = p.complete() 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: kitty_gfx.CommandParser, + + pub fn deinit(self: *State) void { + switch (self.*) { + .inactive, .ignore, .identify => {}, + .kitty => |*v| v.deinit(), + } + } +}; + +/// Possible APC commands. +pub const Command = union(enum) { + kitty: kitty_gfx.Command, + + pub fn deinit(self: *Command, alloc: Allocator) void { + switch (self.*) { + .kitty => |*v| v.deinit(alloc), + } + } +}; + +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" { + 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 "valid Kitty command" { + 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); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index a1b6f6c59..771fcab42 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -5,6 +5,7 @@ const stream = @import("stream.zig"); const ansi = @import("ansi.zig"); const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); +pub const apc = @import("apc.zig"); pub const point = @import("point.zig"); pub const color = @import("color.zig"); pub const kitty = @import("kitty.zig"); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index ed016355c..74a4f9476 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1048,15 +1048,11 @@ const StreamHandler = struct { grid_size: *renderer.GridSize, terminal: *terminal.Terminal, - /// The APC command data. This is always heap-allocated and freed on - /// apcEnd because APC commands are so very rare. - apc_data: std.ArrayListUnmanaged(u8) = .{}, - apc_state: enum { - inactive, - ignore, - verify, - collect, - } = .inactive, + /// 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: terminal.apc.Handler = .{}, /// This is set to true when a message was written to the writer /// mailbox. This can be used by callers to determine if they need @@ -1064,7 +1060,7 @@ const StreamHandler = struct { writer_messaged: bool = false, pub fn deinit(self: *StreamHandler) void { - self.apc_data.deinit(self.alloc); + self.apc.deinit(); } inline fn queueRender(self: *StreamHandler) !void { @@ -1077,49 +1073,17 @@ const StreamHandler = struct { } pub fn apcStart(self: *StreamHandler) !void { - assert(self.apc_data.items.len == 0); - self.apc_state = .verify; + self.apc.start(); } pub fn apcPut(self: *StreamHandler, byte: u8) !void { - switch (self.apc_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, - - // Verify it is a command we expect - .verify => { - switch (byte) { - 'G' => {}, - else => { - log.warn("unrecognized APC command, first byte: {x}", .{byte}); - self.apc_state = .ignore; - return; - }, - } - - self.apc_state = .collect; - }, - - .collect => {}, - } - - try self.apc_data.append(self.alloc, byte); - - // Prevent DoS attack. - const limit = 100 * 1024 * 1024; // 100MB - if (self.apc_data.items.len > limit) { - log.warn("APC command too large, ignoring", .{}); - self.apc_state = .ignore; - self.apc_data.clearAndFree(self.alloc); - } + self.apc.feed(self.alloc, byte); } pub fn apcEnd(self: *StreamHandler) !void { - self.apc_state = .inactive; - self.apc_data.clearAndFree(self.alloc); + var cmd = self.apc.end() orelse return; + defer cmd.deinit(self.alloc); + log.warn("APC command: {}", .{cmd}); } pub fn print(self: *StreamHandler, ch: u21) !void {