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`.pull/12352/head
parent
0f7cd84b88
commit
d3775d1ed0
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||||
const build_options = @import("terminal_options");
|
const build_options = @import("terminal_options");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const glyph = @import("apc/glyph.zig");
|
||||||
const kitty_gfx = @import("kitty/graphics.zig");
|
const kitty_gfx = @import("kitty/graphics.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.terminal_apc);
|
const log = std.log.scoped(.terminal_apc);
|
||||||
|
|
@ -18,6 +19,7 @@ pub const Handler = struct {
|
||||||
/// use `.initFull`.
|
/// use `.initFull`.
|
||||||
max_bytes: std.EnumMap(Protocol, usize) = .initFullWith(.{
|
max_bytes: std.EnumMap(Protocol, usize) = .initFullWith(.{
|
||||||
.kitty = Protocol.defaultMaxBytes(.kitty),
|
.kitty = Protocol.defaultMaxBytes(.kitty),
|
||||||
|
.glyph = Protocol.defaultMaxBytes(.glyph),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
pub fn deinit(self: *Handler) void {
|
pub fn deinit(self: *Handler) void {
|
||||||
|
|
@ -26,7 +28,7 @@ pub const Handler = struct {
|
||||||
|
|
||||||
pub fn start(self: *Handler) void {
|
pub fn start(self: *Handler) void {
|
||||||
self.state.deinit();
|
self.state.deinit();
|
||||||
self.state = .{ .identify = {} };
|
self.state = .{ .identify = .{} };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn feed(self: *Handler, alloc: Allocator, byte: u8) void {
|
pub fn feed(self: *Handler, alloc: Allocator, byte: u8) void {
|
||||||
|
|
@ -38,21 +40,45 @@ pub const Handler = struct {
|
||||||
.ignore => return,
|
.ignore => return,
|
||||||
|
|
||||||
// We identify the APC command by the first byte.
|
// We identify the APC command by the first byte.
|
||||||
.identify => {
|
.identify => |*id| id: {
|
||||||
switch (byte) {
|
// Kitty graphics is detected immediately on the `G` byte,
|
||||||
// Kitty graphics protocol
|
// since commands begin immediately after with no termination
|
||||||
'G' => self.state = if (comptime build_options.kitty_graphics)
|
// character after the 'G'.
|
||||||
.{ .kitty = .init(
|
if (comptime build_options.kitty_graphics) {
|
||||||
|
if (id.len == 0 and byte == 'G') {
|
||||||
|
self.state = .{ .kitty = .init(
|
||||||
alloc,
|
alloc,
|
||||||
self.max_bytes.get(.kitty) orelse
|
self.max_bytes.get(.kitty) orelse
|
||||||
Protocol.defaultMaxBytes(.kitty),
|
Protocol.defaultMaxBytes(.kitty),
|
||||||
) }
|
) };
|
||||||
else
|
break :id;
|
||||||
.ignore,
|
}
|
||||||
|
|
||||||
// Unknown
|
|
||||||
else => self.state = .ignore,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
.kitty => |*p| if (comptime build_options.kitty_graphics) {
|
||||||
|
|
@ -62,6 +88,12 @@ pub const Handler = struct {
|
||||||
self.state = .ignore;
|
self.state = .ignore;
|
||||||
};
|
};
|
||||||
} else unreachable,
|
} 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 };
|
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) {
|
pub const State = union(enum) {
|
||||||
/// We're not in the middle of an APC command yet.
|
/// 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
|
/// We got an unrecognized APC sequence or the APC sequence we
|
||||||
/// recognized became invalid. We're just dropping bytes.
|
/// recognized became invalid. We're just dropping bytes.
|
||||||
ignore: void,
|
ignore,
|
||||||
|
|
||||||
/// We're waiting to identify the APC sequence. This is done by
|
/// We're waiting to identify the APC sequence. The way this is done
|
||||||
/// inspecting the first byte of the sequence.
|
/// is pretty fluid depending on supported APC protocols, but for now
|
||||||
identify: void,
|
/// 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 graphics protocol
|
||||||
kitty: if (build_options.kitty_graphics)
|
kitty: if (build_options.kitty_graphics)
|
||||||
|
|
@ -108,9 +159,13 @@ pub const State = union(enum) {
|
||||||
else
|
else
|
||||||
void,
|
void,
|
||||||
|
|
||||||
|
/// Glyph protocol
|
||||||
|
glyph: glyph.CommandParser,
|
||||||
|
|
||||||
pub fn deinit(self: *State) void {
|
pub fn deinit(self: *State) void {
|
||||||
switch (self.*) {
|
switch (self.*) {
|
||||||
.inactive, .ignore, .identify => {},
|
.inactive, .ignore, .identify => {},
|
||||||
|
.glyph => |*v| v.deinit(),
|
||||||
.kitty => |*v| if (comptime build_options.kitty_graphics)
|
.kitty => |*v| if (comptime build_options.kitty_graphics)
|
||||||
v.deinit()
|
v.deinit()
|
||||||
else
|
else
|
||||||
|
|
@ -122,6 +177,7 @@ pub const State = union(enum) {
|
||||||
/// Possible APC command types.
|
/// Possible APC command types.
|
||||||
pub const Protocol = enum {
|
pub const Protocol = enum {
|
||||||
kitty,
|
kitty,
|
||||||
|
glyph,
|
||||||
|
|
||||||
/// Returns the default maximum bytes for the given protocol.
|
/// Returns the default maximum bytes for the given protocol.
|
||||||
pub fn defaultMaxBytes(self: Protocol) usize {
|
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
|
// Kitty graphics payloads can be very large (e.g. full images
|
||||||
// encoded as base64), so the default is set to 65 MiB.
|
// encoded as base64), so the default is set to 65 MiB.
|
||||||
.kitty => 65 * 1024 * 1024,
|
.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
|
else
|
||||||
void,
|
void,
|
||||||
|
|
||||||
|
glyph: glyph.Request,
|
||||||
|
|
||||||
pub fn deinit(self: *Command, alloc: Allocator) void {
|
pub fn deinit(self: *Command, alloc: Allocator) void {
|
||||||
switch (self.*) {
|
switch (self.*) {
|
||||||
.kitty => |*v| if (comptime build_options.kitty_graphics)
|
.kitty => |*v| if (comptime build_options.kitty_graphics)
|
||||||
v.deinit(alloc)
|
v.deinit(alloc)
|
||||||
else
|
else
|
||||||
unreachable,
|
unreachable,
|
||||||
|
|
||||||
|
.glyph => |*v| v.deinit(alloc),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -246,3 +310,66 @@ test "valid Kitty command" {
|
||||||
defer cmd.deinit(alloc);
|
defer cmd.deinit(alloc);
|
||||||
try testing.expect(cmd == .kitty);
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
//! # 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 current wire protocol surface parsed and formatted
|
||||||
|
//! by the glyph APC modules.
|
||||||
|
//!
|
||||||
|
//! ## 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=<list> ESC \`
|
||||||
|
//!
|
||||||
|
//! `fmt` is a comma-separated list of supported payload format names:
|
||||||
|
//! - `glyf` — TrueType simple glyphs (required in v1)
|
||||||
|
//! - `colrv0` — COLR v0 layered flat-colour glyphs
|
||||||
|
//! - `colrv1` — COLR v1 paint-graph glyphs
|
||||||
|
//!
|
||||||
|
//! Order is not significant. An empty `fmt=` means the terminal recognizes
|
||||||
|
//! Glyph Protocol but currently advertises no payload formats. Clients must
|
||||||
|
//! ignore unknown format names.
|
||||||
|
//!
|
||||||
|
//! 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=<list> ESC \`
|
||||||
|
//!
|
||||||
|
//! `status` is a comma-separated list of coverage names:
|
||||||
|
//! - empty — nothing renders this codepoint (tofu)
|
||||||
|
//! - `system` — a system font covers it
|
||||||
|
//! - `glossary` — a session registration covers it
|
||||||
|
//! - `system,glossary` — both; the registration shadows the system font
|
||||||
|
//!
|
||||||
|
//! Non-PUA codepoints can only report empty or `system`. Clients must ignore
|
||||||
|
//! unknown coverage names.
|
||||||
|
//!
|
||||||
|
//! ## Register (`r`)
|
||||||
|
//!
|
||||||
|
//! Registers a glyph outline at a Private Use Area codepoint.
|
||||||
|
//!
|
||||||
|
//! Request:
|
||||||
|
//! `ESC _ 25a1 ; r ; cp=<hex> [; fmt=glyf] [; reply=<0|1|2>]
|
||||||
|
//! [; upm=<int>] [; aw=<int>] [; lh=<int>] [; width=<1|2>]
|
||||||
|
//! [; size=<height|advance|contain|cover|stretch>]
|
||||||
|
//! [; align=<start|center|end>,<start|center|end|baseline>]
|
||||||
|
//! [; pad=<top>,<right>,<bottom>,<left>] ; <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+E000–U+F8FF, U+F0000–U+FFFFD, or U+100000–U+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.
|
||||||
|
//! - `reply` — response verbosity:
|
||||||
|
//! `1` (default) = success + failure replies
|
||||||
|
//! `2` = failure replies only (silent success)
|
||||||
|
//! `0` = no replies (fire-and-forget)
|
||||||
|
//! - `upm` — units-per-em for the coordinate space. Default 1000.
|
||||||
|
//! - `aw` — authored advance width in upm units. Default `upm`.
|
||||||
|
//! - `lh` — authored line height in upm units. Default `upm`.
|
||||||
|
//! - `width` — Unicode/wcwidth cell width. Must be `1` or `2`; default `1`.
|
||||||
|
//! This is authoritative for cursor advance, wrapping, and
|
||||||
|
//! selection geometry.
|
||||||
|
//! - `size` — scale policy. Default `height`.
|
||||||
|
//! - `align` — horizontal and vertical placement within the render span.
|
||||||
|
//! Default `center,center`.
|
||||||
|
//! - `pad` — fractional insets from the render span edges. Default
|
||||||
|
//! `0,0,0,0`; degenerate padding is treated as no padding.
|
||||||
|
//! - payload — base64-encoded payload for the selected `fmt`.
|
||||||
|
//!
|
||||||
|
//! 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, Y-up, with `y=0` at the baseline;
|
||||||
|
//! the terminal scales and positions at render time using `aw`, `lh`,
|
||||||
|
//! `width`, `size`, `align`, and `pad`.
|
||||||
|
//!
|
||||||
|
//! `colrv0` and `colrv1` wrap OpenType `COLR`/`CPAL` data together with the
|
||||||
|
//! simple-glyph outlines they reference. `colrv0` uses layered flat colours;
|
||||||
|
//! `colrv1` uses the OpenType paint graph and may omit `CPAL` if it does not
|
||||||
|
//! reference palette indices.
|
||||||
|
//!
|
||||||
|
//! 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://raw.githubusercontent.com/raphamorim/rio/779dba839dbb76c551f2efa852b82a2ed669101b/specs/glyph-protocol.md>
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,726 @@
|
||||||
|
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,
|
||||||
|
|
||||||
|
/// Requested reply verbosity for registration.
|
||||||
|
reply,
|
||||||
|
|
||||||
|
/// Units-per-em for the glyph coordinate system.
|
||||||
|
upm,
|
||||||
|
|
||||||
|
/// Authored advance width in units-per-em units.
|
||||||
|
aw,
|
||||||
|
|
||||||
|
/// Authored line height in units-per-em units.
|
||||||
|
lh,
|
||||||
|
|
||||||
|
/// Unicode cell width for terminal layout.
|
||||||
|
width,
|
||||||
|
|
||||||
|
/// Glyph scale policy.
|
||||||
|
size,
|
||||||
|
|
||||||
|
/// Glyph placement within the render span.
|
||||||
|
@"align",
|
||||||
|
|
||||||
|
/// Fractional insets from the render span edges.
|
||||||
|
pad,
|
||||||
|
|
||||||
|
/// Return the decoded Zig type for a register option.
|
||||||
|
pub fn Type(comptime self: Option) type {
|
||||||
|
return switch (self) {
|
||||||
|
.cp => u21,
|
||||||
|
.fmt => Format,
|
||||||
|
.reply => Reply,
|
||||||
|
.upm => u32,
|
||||||
|
.aw => u32,
|
||||||
|
.lh => u32,
|
||||||
|
.width => Width,
|
||||||
|
.size => Size,
|
||||||
|
.@"align" => Align,
|
||||||
|
.pad => Pad,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
.reply => .all,
|
||||||
|
.upm => 1000,
|
||||||
|
.aw => null,
|
||||||
|
.lh => null,
|
||||||
|
.width => .narrow,
|
||||||
|
.size => .height,
|
||||||
|
.@"align" => .{},
|
||||||
|
.pad => .{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
.reply => Reply.init(value) orelse .all,
|
||||||
|
.upm => std.fmt.parseInt(u32, value, 10) catch null,
|
||||||
|
.aw => std.fmt.parseInt(u32, value, 10) catch null,
|
||||||
|
.lh => std.fmt.parseInt(u32, value, 10) catch null,
|
||||||
|
.width => Width.init(value),
|
||||||
|
.size => Size.init(value),
|
||||||
|
.@"align" => Align.init(value),
|
||||||
|
.pad => Pad.init(value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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 switch (option) {
|
||||||
|
.aw, .lh => self.get(.upm),
|
||||||
|
else => 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Register command width override for terminal layout.
|
||||||
|
pub const Width = enum(u2) {
|
||||||
|
/// One terminal cell.
|
||||||
|
narrow = 1,
|
||||||
|
|
||||||
|
/// Two terminal cells.
|
||||||
|
wide = 2,
|
||||||
|
|
||||||
|
/// Parse the register command width from its single-digit encoding.
|
||||||
|
pub fn init(value: []const u8) ?Width {
|
||||||
|
if (value.len != 1) return null;
|
||||||
|
return switch (value[0]) {
|
||||||
|
'1' => .narrow,
|
||||||
|
'2' => .wide,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Register command glyph scale policy.
|
||||||
|
pub const Size = enum {
|
||||||
|
height,
|
||||||
|
advance,
|
||||||
|
contain,
|
||||||
|
cover,
|
||||||
|
stretch,
|
||||||
|
|
||||||
|
/// Parse a glyph scale policy name.
|
||||||
|
pub fn init(value: []const u8) ?Size {
|
||||||
|
return std.meta.stringToEnum(Size, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Register command glyph placement within the render span.
|
||||||
|
pub const Align = struct {
|
||||||
|
horizontal: Horizontal = .center,
|
||||||
|
vertical: Vertical = .center,
|
||||||
|
|
||||||
|
pub const Horizontal = enum {
|
||||||
|
start,
|
||||||
|
center,
|
||||||
|
end,
|
||||||
|
|
||||||
|
fn init(value: []const u8) ?Horizontal {
|
||||||
|
return std.meta.stringToEnum(Horizontal, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Vertical = enum {
|
||||||
|
start,
|
||||||
|
center,
|
||||||
|
end,
|
||||||
|
baseline,
|
||||||
|
|
||||||
|
fn init(value: []const u8) ?Vertical {
|
||||||
|
return std.meta.stringToEnum(Vertical, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parse an align value in `<horizontal>,<vertical>` form.
|
||||||
|
pub fn init(value: []const u8) ?Align {
|
||||||
|
var it = std.mem.splitScalar(u8, value, ',');
|
||||||
|
const horizontal = Horizontal.init(it.next() orelse return null) orelse return null;
|
||||||
|
const vertical = Vertical.init(it.next() orelse return null) orelse return null;
|
||||||
|
if (it.next() != null) return null;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.horizontal = horizontal,
|
||||||
|
.vertical = vertical,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Register command fractional insets from the render span edges.
|
||||||
|
pub const Pad = struct {
|
||||||
|
top: f64 = 0,
|
||||||
|
right: f64 = 0,
|
||||||
|
bottom: f64 = 0,
|
||||||
|
left: f64 = 0,
|
||||||
|
|
||||||
|
/// Parse a pad value in `<top>,<right>,<bottom>,<left>` form.
|
||||||
|
pub fn init(value: []const u8) ?Pad {
|
||||||
|
var it = std.mem.splitScalar(u8, value, ',');
|
||||||
|
const top = parseFraction(it.next() orelse return null) orelse return null;
|
||||||
|
const right = parseFraction(it.next() orelse return null) orelse return null;
|
||||||
|
const bottom = parseFraction(it.next() orelse return null) orelse return null;
|
||||||
|
const left = parseFraction(it.next() orelse return null) orelse return null;
|
||||||
|
if (it.next() != null) return null;
|
||||||
|
|
||||||
|
// Glyph Protocol §8.5.2: "If `l + r ≥ 1` or `t + b ≥ 1`
|
||||||
|
// the terminal MUST treat the request as if `pad=0,0,0,0`."
|
||||||
|
if (left + right >= 1 or top + bottom >= 1) return .{};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.top = top,
|
||||||
|
.right = right,
|
||||||
|
.bottom = bottom,
|
||||||
|
.left = left,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse one pad component from the spec's `0.0`–`1.0` fractional range.
|
||||||
|
/// Top/bottom fractions are relative to cell height; left/right fractions
|
||||||
|
/// are relative to render span width.
|
||||||
|
fn parseFraction(value: []const u8) ?f64 {
|
||||||
|
const result = std.fmt.parseFloat(f64, value) catch return null;
|
||||||
|
if (!(result >= 0 and result <= 1)) return null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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 command with sizing and placement options" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var cmd = try testParse(
|
||||||
|
testing.allocator,
|
||||||
|
"r;cp=e0a0;upm=2048;aw=1024;lh=1536;width=2;size=contain;align=end,baseline;pad=0.1,0.2,0.3,0.4;QQ==",
|
||||||
|
);
|
||||||
|
defer cmd.deinit(testing.allocator);
|
||||||
|
|
||||||
|
try testing.expect(cmd == .register);
|
||||||
|
try testing.expectEqual(@as(u32, 2048), cmd.register.get(.upm).?);
|
||||||
|
try testing.expectEqual(@as(u32, 1024), cmd.register.get(.aw).?);
|
||||||
|
try testing.expectEqual(@as(u32, 1536), cmd.register.get(.lh).?);
|
||||||
|
try testing.expectEqual(Width.wide, cmd.register.get(.width).?);
|
||||||
|
try testing.expectEqual(Size.contain, cmd.register.get(.size).?);
|
||||||
|
try testing.expectEqual(Align{
|
||||||
|
.horizontal = .end,
|
||||||
|
.vertical = .baseline,
|
||||||
|
}, cmd.register.get(.@"align").?);
|
||||||
|
try testing.expectEqual(Pad{
|
||||||
|
.top = 0.1,
|
||||||
|
.right = 0.2,
|
||||||
|
.bottom = 0.3,
|
||||||
|
.left = 0.4,
|
||||||
|
}, cmd.register.get(.pad).?);
|
||||||
|
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.expect(Option.aw.default() == null);
|
||||||
|
try testing.expect(Option.lh.default() == null);
|
||||||
|
try testing.expectEqual(Width.narrow, Option.width.default().?);
|
||||||
|
try testing.expectEqual(Size.height, Option.size.default().?);
|
||||||
|
try testing.expectEqual(Align{}, Option.@"align".default().?);
|
||||||
|
try testing.expectEqual(Pad{}, Option.pad.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(@as(u32, 1000), cmd.register.get(.aw).?);
|
||||||
|
try testing.expectEqual(@as(u32, 1000), cmd.register.get(.lh).?);
|
||||||
|
try testing.expectEqual(Width.narrow, cmd.register.get(.width).?);
|
||||||
|
try testing.expectEqual(Size.height, cmd.register.get(.size).?);
|
||||||
|
try testing.expectEqual(Align{}, cmd.register.get(.@"align").?);
|
||||||
|
try testing.expectEqual(Pad{}, cmd.register.get(.pad).?);
|
||||||
|
try testing.expectEqual(Reply.all, cmd.register.get(.reply).?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "register command aw and lh default to upm" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var cmd = try testParse(testing.allocator, "r;cp=e0a0;upm=2048;QQ==");
|
||||||
|
defer cmd.deinit(testing.allocator);
|
||||||
|
|
||||||
|
try testing.expect(cmd == .register);
|
||||||
|
try testing.expectEqual(@as(u32, 2048), cmd.register.get(.aw).?);
|
||||||
|
try testing.expectEqual(@as(u32, 2048), cmd.register.get(.lh).?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "register command invalid sizing and placement options" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var cmd = try testParse(
|
||||||
|
testing.allocator,
|
||||||
|
"r;cp=e0a0;width=3;size=invalid;align=center,middle;pad=0,1.2,0,0;QQ==",
|
||||||
|
);
|
||||||
|
defer cmd.deinit(testing.allocator);
|
||||||
|
|
||||||
|
try testing.expect(cmd == .register);
|
||||||
|
try testing.expect(cmd.register.get(.width) == null);
|
||||||
|
try testing.expect(cmd.register.get(.size) == null);
|
||||||
|
try testing.expect(cmd.register.get(.@"align") == null);
|
||||||
|
try testing.expect(cmd.register.get(.pad) == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "register command degenerate padding defaults to no padding" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var cmd = try testParse(
|
||||||
|
testing.allocator,
|
||||||
|
"r;cp=e0a0;pad=0.4,0.2,0.6,0.1;QQ==",
|
||||||
|
);
|
||||||
|
defer cmd.deinit(testing.allocator);
|
||||||
|
|
||||||
|
try testing.expect(cmd == .register);
|
||||||
|
try testing.expectEqual(Pad{}, cmd.register.get(.pad).?);
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Query response coverage state for a codepoint.
|
||||||
|
pub const Coverage = packed struct(u2) {
|
||||||
|
/// A system font covers the codepoint.
|
||||||
|
system: bool = false,
|
||||||
|
|
||||||
|
/// A session glyph registration covers the codepoint.
|
||||||
|
glossary: bool = false,
|
||||||
|
|
||||||
|
/// No system font or registered glyph covers the codepoint.
|
||||||
|
pub const free: Coverage = .{};
|
||||||
|
|
||||||
|
/// Parse the query response coverage list from its comma-separated form.
|
||||||
|
/// Unknown coverage names are ignored for forward compatibility.
|
||||||
|
pub fn init(value: []const u8) ?Coverage {
|
||||||
|
var result: Coverage = .free;
|
||||||
|
var it = std.mem.splitScalar(u8, value, ',');
|
||||||
|
while (it.next()) |name| {
|
||||||
|
if (std.mem.eql(u8, name, "system")) {
|
||||||
|
result.system = true;
|
||||||
|
} else if (std.mem.eql(u8, name, "glossary")) {
|
||||||
|
result.glossary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
reason: ?Reason = null,
|
||||||
|
|
||||||
|
/// Register error reason codes defined by Glyph Protocol §6.2.
|
||||||
|
pub const Reason = union(enum) {
|
||||||
|
/// `cp` is not in any PUA range.
|
||||||
|
out_of_namespace,
|
||||||
|
|
||||||
|
/// Payload contains composite glyphs.
|
||||||
|
composite_unsupported,
|
||||||
|
|
||||||
|
/// Payload contains hinting instructions.
|
||||||
|
hinting_unsupported,
|
||||||
|
|
||||||
|
/// Payload failed to parse as the declared `fmt`.
|
||||||
|
malformed_payload,
|
||||||
|
|
||||||
|
/// Payload exceeds 64 KiB after base64 decoding.
|
||||||
|
payload_too_large,
|
||||||
|
|
||||||
|
/// A reason code not known by this version of Ghostty.
|
||||||
|
other: []const u8,
|
||||||
|
|
||||||
|
/// Return the wire-format reason name.
|
||||||
|
pub fn name(self: Reason) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.out_of_namespace => "out_of_namespace",
|
||||||
|
.composite_unsupported => "composite_unsupported",
|
||||||
|
.hinting_unsupported => "hinting_unsupported",
|
||||||
|
.malformed_payload => "malformed_payload",
|
||||||
|
.payload_too_large => "payload_too_large",
|
||||||
|
.other => |value| value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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| {
|
||||||
|
// From the spec:
|
||||||
|
// Order is not significant; clients MUST treat the value as a
|
||||||
|
// set. An empty fmt= value means the terminal recognises
|
||||||
|
// Glyph Protocol but currently advertises no payload formats
|
||||||
|
// — every r will be rejected. Clients MUST ignore names they
|
||||||
|
// do not recognise rather than failing the reply, so future
|
||||||
|
// format names are forward- compatible.
|
||||||
|
try writer.writeAll("s;fmt=");
|
||||||
|
var first = true;
|
||||||
|
if (r.fmt.glyf) {
|
||||||
|
first = false;
|
||||||
|
try writer.writeAll("glyf");
|
||||||
|
}
|
||||||
|
if (r.fmt.colrv0) {
|
||||||
|
if (!first) try writer.writeByte(',');
|
||||||
|
first = false;
|
||||||
|
try writer.writeAll("colrv0");
|
||||||
|
}
|
||||||
|
if (r.fmt.colrv1) {
|
||||||
|
if (!first) try writer.writeByte(',');
|
||||||
|
first = false;
|
||||||
|
try writer.writeAll("colrv1");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.query => |r| {
|
||||||
|
// status is a comma-separated list of coverage names — the
|
||||||
|
// set of sources that can render cp in this session. Order is
|
||||||
|
// not significant; clients MUST treat the value as a set.
|
||||||
|
try writer.print("q;cp={x};status=", .{r.cp});
|
||||||
|
var first = true;
|
||||||
|
if (r.status.system) {
|
||||||
|
first = false;
|
||||||
|
try writer.writeAll("system");
|
||||||
|
}
|
||||||
|
if (r.status.glossary) {
|
||||||
|
if (!first) try writer.writeByte(',');
|
||||||
|
try writer.writeAll("glossary");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.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.name());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.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 default to no advertised formats" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const Formats = Response.Support.Formats;
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(u8, 0), @as(u8, @bitCast(Formats{})));
|
||||||
|
}
|
||||||
|
|
||||||
|
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=glyf,colrv0\x1b\\", writer.buffered());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "response support formatWire with no formats" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
var writer: std.Io.Writer = .fixed(&buf);
|
||||||
|
|
||||||
|
const resp: Response = .{ .support = .{ .fmt = .{} } };
|
||||||
|
try resp.formatWire(&writer);
|
||||||
|
try testing.expectEqualStrings("\x1b_25a1;s;fmt=\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 = .{ .system = true, .glossary = true } } };
|
||||||
|
try resp.formatWire(&writer);
|
||||||
|
try testing.expectEqualStrings("\x1b_25a1;q;cp=e0a0;status=system,glossary\x1b\\", writer.buffered());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "response query formatWire with no coverage" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
var writer: std.Io.Writer = .fixed(&buf);
|
||||||
|
|
||||||
|
const resp: Response = .{ .query = .{ .cp = 0xE0A0, .status = .{} } };
|
||||||
|
try resp.formatWire(&writer);
|
||||||
|
try testing.expectEqualStrings("\x1b_25a1;q;cp=e0a0;status=\x1b\\", writer.buffered());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "coverage parses comma-separated names" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
try testing.expectEqual(Coverage{}, Coverage.init("").?);
|
||||||
|
try testing.expectEqual(Coverage{ .system = true }, Coverage.init("system").?);
|
||||||
|
try testing.expectEqual(Coverage{ .glossary = true }, Coverage.init("glossary").?);
|
||||||
|
try testing.expectEqual(Coverage{ .system = true, .glossary = true }, Coverage.init("system,glossary").?);
|
||||||
|
try testing.expectEqual(Coverage{ .system = true, .glossary = true }, Coverage.init("glossary,system").?);
|
||||||
|
try testing.expectEqual(Coverage{ .system = true }, Coverage.init("system,future").?);
|
||||||
|
try testing.expectEqual(Coverage{}, Coverage.init("future").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "register reason names" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const Reason = Response.Register.Reason;
|
||||||
|
|
||||||
|
try testing.expectEqualStrings("out_of_namespace", Reason.out_of_namespace.name());
|
||||||
|
try testing.expectEqualStrings("composite_unsupported", Reason.composite_unsupported.name());
|
||||||
|
try testing.expectEqualStrings("hinting_unsupported", Reason.hinting_unsupported.name());
|
||||||
|
try testing.expectEqualStrings("malformed_payload", Reason.malformed_payload.name());
|
||||||
|
try testing.expectEqualStrings("payload_too_large", Reason.payload_too_large.name());
|
||||||
|
try testing.expectEqualStrings("future_reason", (Reason{ .other = "future_reason" }).name());
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
@ -679,6 +679,8 @@ pub const Handler = struct {
|
||||||
if (final.len > 3) self.writePty(final[0 .. final.len - 1 :0]);
|
if (final.len > 3) self.writePty(final[0 .. final.len - 1 :0]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.glyph => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -559,6 +559,8 @@ pub const StreamHandler = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.glyph => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue