mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-30 19:11:39 +00:00
terminal: glyph protocol parser and response encoder
This adds the core parse/encode for the still in-development and experimental terminal glyph protocol: https://github.com/raphamorim/rio/pull/1542 Up to version 1.9. The only cross-cutting change necessary was changing the APC identification logic which previously only looked at a single byte to support multi-byte identifiers since the glyph protocol uses `25a1`.
This commit is contained in:
@@ -2,6 +2,7 @@ const std = @import("std");
|
||||
const build_options = @import("terminal_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const glyph = @import("apc/glyph.zig");
|
||||
const kitty_gfx = @import("kitty/graphics.zig");
|
||||
|
||||
const log = std.log.scoped(.terminal_apc);
|
||||
@@ -18,6 +19,7 @@ pub const Handler = struct {
|
||||
/// use `.initFull`.
|
||||
max_bytes: std.EnumMap(Protocol, usize) = .initFullWith(.{
|
||||
.kitty = Protocol.defaultMaxBytes(.kitty),
|
||||
.glyph = Protocol.defaultMaxBytes(.glyph),
|
||||
}),
|
||||
|
||||
pub fn deinit(self: *Handler) void {
|
||||
@@ -26,7 +28,7 @@ pub const Handler = struct {
|
||||
|
||||
pub fn start(self: *Handler) void {
|
||||
self.state.deinit();
|
||||
self.state = .{ .identify = {} };
|
||||
self.state = .{ .identify = .{} };
|
||||
}
|
||||
|
||||
pub fn feed(self: *Handler, alloc: Allocator, byte: u8) void {
|
||||
@@ -38,21 +40,45 @@ 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') {
|
||||
self.state = .{ .kitty = .init(
|
||||
alloc,
|
||||
self.max_bytes.get(.kitty) orelse
|
||||
Protocol.defaultMaxBytes(.kitty),
|
||||
) }
|
||||
else
|
||||
.ignore,
|
||||
|
||||
// Unknown
|
||||
else => self.state = .ignore,
|
||||
) };
|
||||
break :id;
|
||||
}
|
||||
}
|
||||
|
||||
// If we hit `;` then identify...
|
||||
if (byte == ';') {
|
||||
const str = id.buf[0..id.len];
|
||||
if (std.mem.eql(u8, str, "25a1")) {
|
||||
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 +88,12 @@ 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;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,21 +118,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,
|
||||
ignore,
|
||||
|
||||
/// We're waiting to identify the APC sequence. This is done by
|
||||
/// inspecting the first byte of the sequence.
|
||||
identify: void,
|
||||
/// 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 +159,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 +177,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 +185,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 +200,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 +310,66 @@ 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user