diff --git a/src/terminal/kitty/graphics.zig b/src/terminal/kitty/graphics.zig index f2c954d9f..137a8952a 100644 --- a/src/terminal/kitty/graphics.zig +++ b/src/terminal/kitty/graphics.zig @@ -4,6 +4,106 @@ //! https://sw.kovidgoyal.net/kitty/graphics-protocol const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// Command parser parses the Kitty graphics protocol escape sequence. +pub const CommandParser = struct { + alloc: Allocator, + kv: std.StringHashMapUnmanaged([2]usize) = .{}, + data: std.ArrayListUnmanaged(u8) = .{}, + data_i: usize = 0, + value_ptr: *[2]usize = undefined, + state: State = .control_key, + + const State = enum { + control_key, + control_value, + data, + }; + + pub fn deinit(self: *CommandParser) void { + self.kv.deinit(self.alloc); + self.data.deinit(self.alloc); + } + + /// Feed a single byte to the parser. + /// + /// The first byte to start parsing should be the byte immediately following + /// the "G" in the APC sequence, i.e. "\x1b_G123" the first byte should + /// be "1". + pub fn feed(self: *CommandParser, c: u8) !void { + switch (self.state) { + .control_key => switch (c) { + // '=' means the key is complete and we're moving to the value. + '=' => { + const key = self.data.items[self.data_i..]; + const gop = try self.kv.getOrPut(self.alloc, key); + self.state = .control_value; + self.value_ptr = gop.value_ptr; + self.data_i = self.data.items.len; + }, + + else => try self.data.append(self.alloc, c), + }, + + .control_value => switch (c) { + // ',' means we're moving to another kv + ',' => { + self.finishValue(); + self.state = .control_key; + }, + + // ';' means we're moving to the data + ';' => { + self.finishValue(); + self.state = .data; + }, + + else => try self.data.append(self.alloc, c), + }, + + .data => try self.data.append(self.alloc, c), + } + + // We always add to our data list because this is our stable + // array of bytes that we'll reference everywhere else. + } + + /// Complete the parsing. This must be called after all the + /// bytes have been fed to the parser. + pub fn complete(self: *CommandParser) !void { + switch (self.state) { + // We can't ever end in the control key state and be valid. + // This means the command looked something like "a=1,b" + .control_key => return error.InvalidFormat, + + // Some commands (i.e. placements) end without extra data so + // we end in the value state. i.e. "a=1,b=2" + .control_value => self.finishValue(), + + // Most commands end in data, i.e. "a=1,b=2;1234" + .data => {}, + } + } + + fn finishValue(self: *CommandParser) void { + self.value_ptr.* = .{ self.data_i, self.data.items.len }; + self.data_i = self.data.items.len; + } +}; + +test "parse" { + const testing = std.testing; + const alloc = testing.allocator; + var p: CommandParser = .{ .alloc = alloc }; + defer p.deinit(); + + const input = "f=24,s=10,v=20"; + for (input) |c| try p.feed(c); + try p.complete(); + + try testing.expectEqual(@as(u32, 3), p.kv.count()); +} pub const Command = struct { control: Control,