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

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:
Mitchell Hashimoto
2026-04-19 14:34:01 -07:00
parent f9a9d33b3a
commit 3d896ca258
6 changed files with 991 additions and 17 deletions

View File

@@ -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);
}

131
src/terminal/apc/glyph.zig Normal file
View File

@@ -0,0 +1,131 @@
//! # Glyph Protocol
//!
//! The Glyph Protocol lets applications register custom glyphs with the
//! terminal at runtime and query whether a given codepoint is already
//! covered by a system font or a prior registration. It eliminates the
//! requirement for users to install patched fonts (e.g. Nerd Fonts) in
//! order to render icons in TUIs.
//!
//! This file documents the wire protocol surface implemented by the parser
//! and response formatter below.
//!
//! ## Transport
//!
//! Messages use APC (Application Program Command) framing.
//! Terminals that do not implement the protocol can safely ignore APC
//! sequences. Every message is prefixed with the identifier `25a1`
//! (U+25A1 WHITE SQUARE — the canonical tofu symbol).
//!
//! ## Framing
//!
//! ```
//! ESC _ 25a1 ; <verb> [ ; key=value ]* [ ; <payload> ] ESC \
//! ```
//!
//! Four verbs are defined:
//!
//! - `s` — support query
//! - `q` — codepoint query
//! - `r` — register a glyph
//! - `c` — clear registrations
//!
//! ## Support (`s`)
//!
//! Detects whether the terminal implements Glyph Protocol and which
//! payload formats it supports.
//!
//! Request: `ESC _ 25a1 ; s ESC \`
//! Response: `ESC _ 25a1 ; s ; fmt=<bitfield> ESC \`
//!
//! `fmt` bits:
//! - bit 0 (`1`): `glyf` — TrueType simple glyphs (required in v1)
//! - bit 1 (`2`): `colrv0` — COLR v0 layered flat-colour glyphs
//! - bit 2 (`4`): `colrv1` — COLR v1 paint-graph glyphs
//!
//! Any reply confirms support; no reply within a timeout means the
//! terminal does not implement the protocol.
//!
//! ## Query (`q`)
//!
//! Asks whether a codepoint is renderable and by whom.
//!
//! Request: `ESC _ 25a1 ; q ; cp=<hex> ESC \`
//! Response: `ESC _ 25a1 ; q ; cp=<hex> ; status=<u8> ESC \`
//!
//! `status` is a two-bit field:
//! - `0` (`free`) — nothing renders this codepoint (tofu)
//! - `1` (`system`) — a system font covers it
//! - `2` (`glossary`) — a session registration covers it
//! - `3` (`both`) — both; the registration shadows the system font
//!
//! ## Register (`r`)
//!
//! Registers a glyph outline at a Private Use Area codepoint.
//!
//! Request:
//! `ESC _ 25a1 ; r ; cp=<hex> [; fmt=glyf] [; upm=<int>]
//! [; reply=<0|1|2>] ; <base64-payload> ESC \`
//!
//! Response:
//! `ESC _ 25a1 ; r ; cp=<hex> ; status=0 ESC \`
//! On error: `status=<nonzero> ; reason=<code>`
//!
//! Parameters:
//! - `cp` — target codepoint (hex). Must be in a PUA range:
//! U+E000U+F8FF, U+F0000U+FFFFD, or U+100000U+10FFFD.
//! Non-PUA values are rejected with `reason=out_of_namespace`.
//! - `fmt` — payload format. Default `glyf`; `colrv0` and `colrv1`
//! are optional and advertised via the `s` reply.
//! - `upm` — units-per-em for the coordinate space. Default 1000.
//! - `reply` — response verbosity:
//! `1` (default) = success + failure replies
//! `2` = failure replies only (silent success)
//! `0` = no replies (fire-and-forget)
//! - payload — base64-encoded `glyf` simple-glyph record.
//!
//! The `glyf` subset accepted:
//! - Simple glyphs only (no composites).
//! - Standard flag encoding (on-curve, off-curve, x/y-short, repeat).
//! - No hinting instructions.
//! - Coordinates are in the `upm` space; the terminal scales to cell size.
//!
//! A second `r` on the same `cp` overwrites the previous registration.
//! `glyf` outlines render in the current foreground colour.
//!
//! ## Clear (`c`)
//!
//! Removes registrations.
//!
//! Single slot: `ESC _ 25a1 ; c ; cp=<hex> ESC \`
//! All slots: `ESC _ 25a1 ; c ESC \`
//!
//! The terminal acks with `status=0` even if the slot was already empty.
//! Clear replies do not echo `cp`. `cp` must be in a PUA range; non-PUA values return
//! `reason=out_of_namespace`.
//!
//! ## Glossary Capacity
//!
//! Each session holds at most 1024 registrations keyed by codepoint.
//! Registrations live for the session duration. A 1025th registration
//! evicts the oldest entry (FIFO). Sessions are isolated: two tabs may
//! independently register the same codepoint.
//!
//! ## Security: PUA-Only Restriction
//!
//! Registration is restricted to the three Unicode Private Use Areas to
//! prevent glyph-spoofing attacks. PUA codepoints never appear in normal
//! text (filenames, URLs, commands), so a registered glyph cannot alter
//! how real text is perceived. The cell buffer always stores the original
//! codepoint — copy/paste, search, and hyperlink detection return the
//! codepoint the application emitted, never the rendered glyph.
//!
//! Reference: <https://rapha.land/introducing-glyph-protocol-for-terminals/>
const std = @import("std");
pub const request = @import("glyph/request.zig");
pub const response = @import("glyph/response.zig");
pub const CommandParser = request.CommandParser;
pub const Request = request.Request;
pub const Response = response.Response;

View File

@@ -0,0 +1,494 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
/// Stateful parser for a single glyph APC payload after the `25a1;` prefix.
pub const CommandParser = struct {
alloc: Allocator,
data: std.ArrayList(u8) = .empty,
/// Maximum bytes the data payload can buffer. This is to prevent
/// malicious input from causing us to allocate too much memory.
max_bytes: usize,
pub const Error = Allocator.Error || error{InvalidFormat};
/// Create a glyph APC parser that buffers the raw command bytes.
pub fn init(alloc: Allocator, max_bytes: usize) CommandParser {
return .{ .alloc = alloc, .max_bytes = max_bytes };
}
/// Release any buffered command bytes owned by the parser.
pub fn deinit(self: *CommandParser) void {
self.data.deinit(self.alloc);
}
/// Append one more byte of APC payload to the buffered command.
pub fn feed(self: *CommandParser, byte: u8) Allocator.Error!void {
if (self.data.items.len >= self.max_bytes) return error.OutOfMemory;
try self.data.append(self.alloc, byte);
}
/// Finish parsing and return an owned request that can outlive the parser.
pub fn complete(self: *CommandParser, alloc: Allocator) Error!Request {
// Normalize bare single-byte verbs like `s` into `s;` so the parsed
// command always has the standard `verb;...` layout.
if (self.data.items.len == 1) try self.data.append(self.alloc, ';');
const raw = try self.data.toOwnedSlice(alloc);
// Ownership of the buffered bytes has moved to `raw`, so clear the
// array list before we build the final command value.
self.data = .empty;
errdefer alloc.free(raw);
return try Request.parse(alloc, raw);
}
};
/// Parsed glyph APC request with the verb classified eagerly.
pub const Request = union(enum) {
/// Support query (bare `s` verb, no options).
support,
/// Codepoint coverage query.
query: Query,
/// Glyph registration request.
register: Register,
/// Registration clear request.
clear: Clear,
/// Query verb payload with lazily-decoded options.
pub const Query = struct {
raw: []const u8,
/// Initialize a query command from owned raw command bytes.
pub fn init(raw: []const u8) Query {
return .{ .raw = raw };
}
/// Options recognized for the glyph query request.
pub const Option = enum {
/// Target Unicode codepoint encoded in hexadecimal.
cp,
/// Return the decoded Zig type for a query option.
pub fn Type(comptime self: Option) type {
return switch (self) {
.cp => u21,
};
}
/// Return the wire-format option key for this query option.
fn key(comptime self: Option) []const u8 {
return @tagName(self);
}
/// Read and decode a query option from the raw option string.
pub fn read(comptime self: Option, raw: []const u8) ?self.Type() {
const value = optionValue(raw, self.key()) orelse return null;
return switch (self) {
.cp => std.fmt.parseInt(u21, value, 16) catch null,
};
}
};
/// Lazily decode a query option on demand.
pub fn get(self: Query, comptime option: Option) ?option.Type() {
return option.read(self.rawOptions());
}
/// Return the raw option portion of a valid query command.
fn rawOptions(self: Query) []const u8 {
assert(self.raw.len >= 2);
assert(self.raw[0] == 'q');
assert(self.raw[1] == ';');
return self.raw[2..];
}
};
/// Register verb payload with lazily-decoded options and optional base64 data.
pub const Register = struct {
raw: []const u8,
payload_idx: usize,
/// Initialize a register command from owned raw command bytes.
pub fn init(raw: []const u8) Register {
assert(raw.len >= 2);
assert(raw[0] == 'r');
assert(raw[1] == ';');
const payload_idx = std.mem.lastIndexOfScalar(u8, raw, ';').?;
assert(payload_idx > 1);
return .{
.raw = raw,
.payload_idx = payload_idx,
};
}
/// Options recognized for the glyph register verb.
pub const Option = enum {
/// Target Unicode codepoint encoded in hexadecimal.
cp,
/// Glyph payload format.
fmt,
/// Units-per-em for the glyph coordinate system.
upm,
/// Requested reply verbosity for registration.
reply,
/// Return the decoded Zig type for a register option.
pub fn Type(comptime self: Option) type {
return switch (self) {
.cp => u21,
.fmt => Format,
.upm => u32,
.reply => Reply,
};
}
/// Return the protocol default value for this option, if any.
pub fn default(comptime self: Option) ?self.Type() {
return switch (self) {
.cp => null,
.fmt => .glyf,
.upm => 1000,
.reply => .all,
};
}
/// Return the wire-format option key for this register option.
fn key(comptime self: Option) []const u8 {
return @tagName(self);
}
/// Read and decode a register option from the raw option string.
pub fn read(comptime self: Option, raw: []const u8) ?self.Type() {
const value = optionValue(raw, self.key()) orelse return null;
return switch (self) {
.cp => std.fmt.parseInt(u21, value, 16) catch null,
.fmt => Format.init(value),
.upm => std.fmt.parseInt(u32, value, 10) catch null,
.reply => Reply.init(value) orelse .all,
};
}
};
/// Lazily decode a register option on demand, applying protocol
/// defaults when the option is omitted.
pub fn get(self: Register, comptime option: Option) ?option.Type() {
const raw = self.rawOptions();
if (optionValue(raw, option.key()) == null) return option.default();
return option.read(raw);
}
/// Return the base64 payload carried by a register request.
///
/// If no payload is present, this returns an empty slice. The returned
/// bytes may still be invalid base64; this function only exposes the raw
/// payload segment and does not validate or decode it.
pub fn payload(self: Register) []const u8 {
assert(self.raw.len >= 2);
assert(self.raw[0] == 'r');
assert(self.raw[1] == ';');
return if (self.payload_idx == self.raw.len)
""
else
self.raw[self.payload_idx + 1 ..];
}
/// Return the raw option portion of a valid register command.
fn rawOptions(self: Register) []const u8 {
assert(self.raw.len >= 2);
assert(self.raw[0] == 'r');
assert(self.raw[1] == ';');
assert(self.payload_idx >= 2);
assert(self.payload_idx <= self.raw.len);
return self.raw[2..self.payload_idx];
}
};
/// Clear verb payload with lazily-decoded options.
pub const Clear = struct {
raw: []const u8,
/// Initialize a clear command from owned raw command bytes.
pub fn init(raw: []const u8) Clear {
return .{ .raw = raw };
}
/// Options recognized for the glyph clear request.
pub const Option = enum {
/// Target Unicode codepoint encoded in hexadecimal.
cp,
/// Return the decoded Zig type for a clear option.
pub fn Type(comptime self: Option) type {
return switch (self) {
.cp => u21,
};
}
/// Return the wire-format option key for this clear option.
fn key(comptime self: Option) []const u8 {
return @tagName(self);
}
/// Read and decode a clear option from the raw option string.
pub fn read(comptime self: Option, raw: []const u8) ?self.Type() {
const value = optionValue(raw, self.key()) orelse return null;
return switch (self) {
.cp => std.fmt.parseInt(u21, value, 16) catch null,
};
}
};
/// Lazily decode a clear option on demand.
pub fn get(self: Clear, comptime option: Option) ?option.Type() {
return option.read(self.rawOptions());
}
/// Return the raw option portion of a valid clear command.
fn rawOptions(self: Clear) []const u8 {
assert(self.raw.len >= 2);
assert(self.raw[0] == 'c');
assert(self.raw[1] == ';');
return self.raw[2..];
}
};
/// Parse an owned glyph APC payload into its eagerly-classified request
/// form.
///
/// The raw format here is strict on its requirements to avoid
/// edge cases: it must contain the request AND the request must
/// end in a semicolon (even if there are no options). The spec itself
/// does not require this but we artificially insert it in our parser
/// to simplify parsing later.
pub fn parse(alloc: Allocator, raw: []const u8) error{InvalidFormat}!Request {
if (raw.len < 2) return error.InvalidFormat;
if (raw[1] != ';') return error.InvalidFormat;
return switch (raw[0]) {
's' => {
alloc.free(raw);
return .support;
},
'q' => .{ .query = .init(raw) },
'r' => .{ .register = .init(raw) },
'c' => .{ .clear = .init(raw) },
else => error.InvalidFormat,
};
}
/// Free the raw bytes retained by any request variant.
pub fn deinit(self: *Request, alloc: Allocator) void {
switch (self.*) {
.support => {},
inline else => |*cmd| if (cmd.raw.len > 0) alloc.free(cmd.raw),
}
}
};
/// Glyph payload formats named by the protocol.
pub const Format = enum {
/// TrueType simple glyph outline data.
glyf,
/// OpenType COLR version 0 layered color glyph data.
colrv0,
/// OpenType COLR version 1 paint graph glyph data.
colrv1,
/// Parse a glyph payload format name.
pub fn init(value: []const u8) ?Format {
return std.meta.stringToEnum(Format, value);
}
};
/// Register command reply verbosity.
pub const Reply = enum(u2) {
/// Suppress both success and failure replies.
none = 0,
/// Emit replies for both success and failure cases.
all = 1,
/// Emit replies only for failure cases.
failures = 2,
/// Parse the register command reply mode from its single-digit encoding.
pub fn init(value: []const u8) ?Reply {
if (value.len != 1) return null;
return switch (value[0]) {
'0' => .none,
'1' => .all,
'2' => .failures,
else => null,
};
}
};
/// Find the last occurrence of `key=value` for a lazily-parsed option list.
fn optionValue(raw: []const u8, comptime key: []const u8) ?[]const u8 {
var remaining = raw;
var result: ?[]const u8 = null;
while (remaining.len > 0) {
// Options are semicolon-delimited, so each loop peels off one segment
// and checks whether it matches the requested key.
const len = std.mem.indexOfScalar(u8, remaining, ';') orelse remaining.len;
const full = remaining[0..len];
if (std.mem.indexOfScalar(u8, full, '=')) |eql_idx| {
if (std.mem.eql(u8, full[0..eql_idx], key)) {
result = full[eql_idx + 1 ..];
}
}
if (len == remaining.len) break;
remaining = remaining[len + 1 ..];
}
return result;
}
fn testParse(alloc: Allocator, data: []const u8) CommandParser.Error!Request {
var parser = CommandParser.init(alloc, 1024 * 1024);
defer parser.deinit();
for (data) |byte| try parser.feed(byte);
return try parser.complete(alloc);
}
test "support command" {
const testing = std.testing;
var cmd = try testParse(testing.allocator, "s");
defer cmd.deinit(testing.allocator);
try testing.expect(cmd == .support);
}
test "query command" {
const testing = std.testing;
var cmd = try testParse(testing.allocator, "q;cp=E0A0");
defer cmd.deinit(testing.allocator);
try testing.expect(cmd == .query);
try testing.expectEqual(@as(u21, 0xE0A0), cmd.query.get(.cp).?);
}
test "register command with payload" {
const testing = std.testing;
var cmd = try testParse(
testing.allocator,
"r;cp=e0a0;fmt=glyf;upm=1000;reply=2;QQ==",
);
defer cmd.deinit(testing.allocator);
try testing.expect(cmd == .register);
try testing.expectEqual(@as(u21, 0xE0A0), cmd.register.get(.cp).?);
try testing.expectEqual(Format.glyf, cmd.register.get(.fmt).?);
try testing.expectEqual(@as(u32, 1000), cmd.register.get(.upm).?);
try testing.expectEqual(Reply.failures, cmd.register.get(.reply).?);
try testing.expectEqualStrings("QQ==", cmd.register.payload());
}
test "register option defaults" {
const testing = std.testing;
const Option = Request.Register.Option;
try testing.expect(Option.cp.default() == null);
try testing.expectEqual(Format.glyf, Option.fmt.default().?);
try testing.expectEqual(@as(u32, 1000), Option.upm.default().?);
try testing.expectEqual(Reply.all, Option.reply.default().?);
}
test "register command defaults" {
const testing = std.testing;
var cmd = try testParse(testing.allocator, "r;cp=e0a0;QQ==");
defer cmd.deinit(testing.allocator);
try testing.expect(cmd == .register);
try testing.expectEqual(@as(u21, 0xE0A0), cmd.register.get(.cp).?);
try testing.expectEqual(Format.glyf, cmd.register.get(.fmt).?);
try testing.expectEqual(@as(u32, 1000), cmd.register.get(.upm).?);
try testing.expectEqual(Reply.all, cmd.register.get(.reply).?);
}
test "register command invalid reply falls back to reply=1" {
const testing = std.testing;
var cmd = try testParse(testing.allocator, "r;cp=e0a0;reply=9;QQ==");
defer cmd.deinit(testing.allocator);
try testing.expect(cmd == .register);
try testing.expectEqual(Reply.all, cmd.register.get(.reply).?);
}
test "register command duplicate options use the last value" {
const testing = std.testing;
var cmd = try testParse(testing.allocator, "r;cp=e0a0;reply=1;reply=2;QQ==");
defer cmd.deinit(testing.allocator);
try testing.expect(cmd == .register);
try testing.expectEqual(Reply.failures, cmd.register.get(.reply).?);
}
test "register command with invalid payload" {
const testing = std.testing;
var cmd = try testParse(
testing.allocator,
"r;cp=e0a0;fmt=glyf;%%%not-base64%%%",
);
defer cmd.deinit(testing.allocator);
try testing.expect(cmd == .register);
try testing.expectEqual(@as(u21, 0xE0A0), cmd.register.get(.cp).?);
try testing.expectEqual(Format.glyf, cmd.register.get(.fmt).?);
try testing.expectEqualStrings("%%%not-base64%%%", cmd.register.payload());
}
test "register response without payload" {
const testing = std.testing;
var cmd = try testParse(
testing.allocator,
"r;cp=E0A0;status=4;reason=out_of_namespace",
);
defer cmd.deinit(testing.allocator);
// Register parsing is request-only, so the final segment is always treated
// as payload rather than as a response field.
try testing.expect(cmd == .register);
try testing.expectEqual(@as(u21, 0xE0A0), cmd.register.get(.cp).?);
try testing.expectEqualStrings("reason=out_of_namespace", cmd.register.payload());
}
test "clear command" {
const testing = std.testing;
var cmd = try testParse(testing.allocator, "c;cp=e0a0");
defer cmd.deinit(testing.allocator);
try testing.expect(cmd == .clear);
try testing.expectEqual(@as(u21, 0xE0A0), cmd.clear.get(.cp).?);
}
test "invalid command" {
const testing = std.testing;
try testing.expectError(
error.InvalidFormat,
testParse(testing.allocator, "x"),
);
}

View File

@@ -0,0 +1,218 @@
const std = @import("std");
/// Query response coverage state for a codepoint.
pub const Coverage = enum(u2) {
/// No system font or registered glyph covers the codepoint.
free = 0,
/// A system font covers the codepoint.
system = 1,
/// A session glyph registration covers the codepoint.
glossary = 2,
/// Both the system font and a session registration cover the codepoint.
both = 3,
/// Parse the query response coverage bitfield from its decimal form.
pub fn init(value: []const u8) ?Coverage {
const raw = std.fmt.parseInt(u2, value, 10) catch return null;
return std.meta.intToEnum(Coverage, raw) catch null;
}
};
/// Response to a glyph APC request, formatted for the wire protocol.
pub const Response = union(enum) {
/// Support query response listing supported payload formats.
support: Support,
/// Codepoint coverage query response.
query: Query,
/// Glyph registration response (success or error).
register: Register,
/// Registration clear response.
clear: Clear,
/// Support query response fields.
pub const Support = struct {
/// Supported payload formats.
fmt: Formats,
pub const Formats = packed struct(u8) {
/// TrueType simple glyph outlines (required in v1).
glyf: bool = false,
/// COLR v0 layered flat-colour glyphs.
colrv0: bool = false,
/// COLR v1 paint-graph glyphs.
colrv1: bool = false,
_padding: u5 = 0,
};
};
/// Codepoint query response fields.
pub const Query = struct {
/// The queried codepoint.
cp: u21,
/// Coverage status for the codepoint.
status: Coverage,
};
/// Register response fields.
pub const Register = struct {
/// The target codepoint of the registration.
cp: u21,
/// Result status of the registration encoded as a decimal u8.
status: Status = .ok,
/// Optional symbolic error reason (e.g. `out_of_namespace`).
reason: ?[]const u8 = null,
};
/// Clear response fields.
pub const Clear = struct {
/// Result status of the clear operation encoded as a decimal u8.
status: Status = .ok,
/// Optional symbolic error reason.
reason: ?[]const u8 = null,
};
/// Status code for register and clear responses.
pub const Status = enum(u8) {
/// The operation completed successfully.
ok = 0,
/// A generic or unspecified error occurred.
err = 1,
_,
};
/// Write the response in the glyph APC wire format to `writer`.
///
/// The framing is: `ESC _ 25a1 ; <verb> ; <key=value>* ESC \`
pub fn formatWire(
self: Response,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
try writer.writeAll("\x1b_25a1;");
switch (self) {
.support => |r| {
try writer.writeAll("s;fmt=");
try writer.print("{d}", .{@as(u8, @bitCast(r.fmt))});
},
.query => |r| {
try writer.print("q;cp={x};status={d}", .{ r.cp, @intFromEnum(r.status) });
},
.register => |r| {
try writer.print("r;cp={x};status={d}", .{ r.cp, @intFromEnum(r.status) });
if (r.reason) |reason| {
try writer.writeAll(";reason=");
try writer.writeAll(reason);
}
},
.clear => |r| {
try writer.print("c;status={d}", .{@intFromEnum(r.status)});
if (r.reason) |reason| {
try writer.writeAll(";reason=");
try writer.writeAll(reason);
}
},
}
try writer.writeAll("\x1b\\");
}
};
test "support formats bit layout" {
const testing = std.testing;
const Formats = Response.Support.Formats;
try testing.expectEqual(@as(u8, 1), @as(u8, @bitCast(Formats{ .glyf = true })));
try testing.expectEqual(@as(u8, 2), @as(u8, @bitCast(Formats{ .colrv0 = true })));
try testing.expectEqual(@as(u8, 4), @as(u8, @bitCast(Formats{ .colrv1 = true })));
try testing.expectEqual(@as(u8, 7), @as(u8, @bitCast(Formats{ .glyf = true, .colrv0 = true, .colrv1 = true })));
}
test "response support formatWire" {
const testing = std.testing;
var buf: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const resp: Response = .{ .support = .{ .fmt = .{ .glyf = true, .colrv0 = true } } };
try resp.formatWire(&writer);
try testing.expectEqualStrings("\x1b_25a1;s;fmt=3\x1b\\", writer.buffered());
}
test "response query formatWire" {
const testing = std.testing;
var buf: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const resp: Response = .{ .query = .{ .cp = 0xE0A0, .status = .both } };
try resp.formatWire(&writer);
try testing.expectEqualStrings("\x1b_25a1;q;cp=e0a0;status=3\x1b\\", writer.buffered());
}
test "response register success formatWire" {
const testing = std.testing;
var buf: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const resp: Response = .{ .register = .{ .cp = 0xE0A0 } };
try resp.formatWire(&writer);
try testing.expectEqualStrings("\x1b_25a1;r;cp=e0a0;status=0\x1b\\", writer.buffered());
}
test "response register error formatWire" {
const testing = std.testing;
var buf: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const resp: Response = .{ .register = .{ .cp = 0xE0A0, .status = .err, .reason = "out_of_namespace" } };
try resp.formatWire(&writer);
try testing.expectEqualStrings("\x1b_25a1;r;cp=e0a0;status=1;reason=out_of_namespace\x1b\\", writer.buffered());
}
test "response register arbitrary status formatWire" {
const testing = std.testing;
var buf: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const resp: Response = .{ .register = .{ .cp = 0xE0A0, .status = @enumFromInt(37), .reason = "payload_too_large" } };
try resp.formatWire(&writer);
try testing.expectEqualStrings("\x1b_25a1;r;cp=e0a0;status=37;reason=payload_too_large\x1b\\", writer.buffered());
}
test "response clear formatWire" {
const testing = std.testing;
var buf: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const resp: Response = .{ .clear = .{} };
try resp.formatWire(&writer);
try testing.expectEqualStrings("\x1b_25a1;c;status=0\x1b\\", writer.buffered());
}
test "response clear error formatWire" {
const testing = std.testing;
var buf: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const resp: Response = .{ .clear = .{ .status = .err, .reason = "out_of_namespace" } };
try resp.formatWire(&writer);
try testing.expectEqualStrings("\x1b_25a1;c;status=1;reason=out_of_namespace\x1b\\", writer.buffered());
}

View File

@@ -679,6 +679,8 @@ pub const Handler = struct {
if (final.len > 3) self.writePty(final[0 .. final.len - 1 :0]);
}
},
.glyph => {},
}
}
};

View File

@@ -559,6 +559,8 @@ pub const StreamHandler = struct {
}
}
},
.glyph => {},
}
}