aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/apc
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:28:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:29:28 +0300
commit4a3302d19a2baa3a33e910e0088c24e780905b90 (patch)
treedcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/terminal/apc
parent550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff)
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/apc')
-rw-r--r--vendor/libghostty-vt/src/terminal/apc/glyph.zig164
-rw-r--r--vendor/libghostty-vt/src/terminal/apc/glyph/AGENTS.md9
-rw-r--r--vendor/libghostty-vt/src/terminal/apc/glyph/Glossary.zig444
-rw-r--r--vendor/libghostty-vt/src/terminal/apc/glyph/execute.zig367
-rw-r--r--vendor/libghostty-vt/src/terminal/apc/glyph/request.zig855
-rw-r--r--vendor/libghostty-vt/src/terminal/apc/glyph/response.zig346
6 files changed, 2185 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/apc/glyph.zig b/vendor/libghostty-vt/src/terminal/apc/glyph.zig
new file mode 100644
index 00000000..727e1ab4
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/apc/glyph.zig
@@ -0,0 +1,164 @@
+//! # 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 execute = @import("glyph/execute.zig").execute;
+
+pub const CommandParser = request.CommandParser;
+pub const Request = request.Request;
+pub const Response = response.Response;
+pub const Glossary = @import("glyph/Glossary.zig");
+
+test {
+ std.testing.refAllDecls(@This());
+}
diff --git a/vendor/libghostty-vt/src/terminal/apc/glyph/AGENTS.md b/vendor/libghostty-vt/src/terminal/apc/glyph/AGENTS.md
new file mode 100644
index 00000000..c81029c0
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/apc/glyph/AGENTS.md
@@ -0,0 +1,9 @@
+# Glyph Protocol
+
+- The specification source of truth is:
+ <https://github.com/raphamorim/rio/blob/main/specs/glyph-protocol.md>
+- A summary of the specification is available in
+ `src/terminal/apc/glyph.zig` at the top.
+- Reference the specification whenever any changes are made to
+ this folder. Prefer the local specification over fetching the
+ latest, unless it is lacking information.
diff --git a/vendor/libghostty-vt/src/terminal/apc/glyph/Glossary.zig b/vendor/libghostty-vt/src/terminal/apc/glyph/Glossary.zig
new file mode 100644
index 00000000..28458dbb
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/apc/glyph/Glossary.zig
@@ -0,0 +1,444 @@
+/// Glossary is the per-terminal storage for Glyph Protocol
+/// codepoints. We use the word Glossary to match up with the spec which
+/// also uses this word.
+const Glossary = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const CircBuf = @import("../../../datastruct/circ_buf.zig").CircBuf;
+const FontGlyph = @import("../../../font/Glyph.zig");
+const Glyf = @import("../../../font/opentype/glyf.zig").Glyf;
+
+const request = @import("request.zig");
+const RegisterReq = request.Request.Register;
+
+const DesignMetrics = FontGlyph.DesignMetrics;
+const Constraint = FontGlyph.RenderOptions.Constraint;
+
+/// Maximum entries allowed in the glossary before eviction.
+/// Defined by the specification.
+pub const max_entries = 1024;
+
+/// An empty glossary with no registered glyphs.
+pub const empty: Glossary = .{ .entries = .empty };
+
+/// Errors that can occur while registering a glossary entry.
+pub const RegisterError = Allocator.Error || error{OutOfNamespace};
+
+/// Errors that can occur while clearing glossary entries.
+pub const ClearError = error{OutOfNamespace};
+
+/// The set of entries in the glossary keyed by the codepoint.
+///
+/// The array hash map preserves insertion order and has O(N)
+/// orderedRemove, so we use it as a FIFO too for eviction when
+/// the glossary is full. Since the specification limits the protocol
+/// to 1024 maximum entries, ordered removal should never be that
+/// expensive.
+///
+/// I'm also operating under the assumption that full glossaries
+/// for a session will be rare, so the eviction cost shouldn't
+/// happen regularly.
+entries: std.AutoArrayHashMapUnmanaged(u21, Entry),
+
+/// Release all glyph entries and hash map storage owned by the glossary.
+pub fn deinit(self: *Glossary, alloc: Allocator) void {
+ for (self.entries.values()) |*entry| entry.deinit(alloc);
+ self.entries.deinit(alloc);
+ self.* = undefined;
+}
+
+/// Register the given glyph entry.
+///
+/// This will act according to the glyph specification
+pub fn register(
+ self: *Glossary,
+ alloc: Allocator,
+ cp: u21,
+ entry: Entry,
+) RegisterError!void {
+ // Validate codepoint according to spec.
+ if (!isPrivateUse(cp)) return error.OutOfNamespace;
+
+ const gop = try self.entries.getOrPut(alloc, cp);
+ if (gop.found_existing) {
+ // Found an existing entry, we need to shift the FIFO so
+ // that this is now the most recent (at the end). This is
+ // O(N) but N is usually small and max N is bounded by the spec.
+ gop.value_ptr.*.deinit(alloc);
+ assert(self.entries.orderedRemove(cp));
+
+ // We already had enough capacity for this key before removing it, so
+ // reinserting the replacement cannot require another allocation.
+ self.entries.putAssumeCapacity(cp, entry);
+ return;
+ }
+
+ // Array hash maps preserve insertion order so always immediately insert.
+ gop.value_ptr.* = entry;
+
+ // Fast, typical path: we fit within the glossary, just return.
+ if (self.entries.count() <= max_entries) return;
+
+ // Slow path: we need to evict.
+ self.entries.values()[0].deinit(alloc);
+ self.entries.orderedRemoveAt(0);
+}
+
+/// Delete a single entry from the glossary. If the entry doesn't exist,
+/// then this does nothing and is safe.
+pub fn delete(
+ self: *Glossary,
+ alloc: Allocator,
+ cp: u21,
+) ClearError!void {
+ if (!isPrivateUse(cp)) return error.OutOfNamespace;
+ const kv = self.entries.fetchOrderedRemove(cp) orelse return;
+ var entry = kv.value;
+ entry.deinit(alloc);
+}
+
+/// Clear all entries from the glossary and free up any underlying
+/// storage.
+pub fn clearAndFree(self: *Glossary, alloc: Allocator) void {
+ for (self.entries.values()) |*entry| entry.deinit(alloc);
+ self.entries.deinit(alloc);
+ self.entries = .empty;
+}
+
+/// Contains returns true if the codepoint is covered by the glossary.
+pub fn contains(self: *Glossary, cp: u21) bool {
+ return self.entries.contains(cp);
+}
+
+/// A single glyph registration entry.
+pub const Entry = struct {
+ /// Stored glyph payload variants.
+ pub const Glyph = union(enum) {
+ glyf: Glyf.Outline,
+ };
+
+ /// The glyph itself. The tagged union only has glyf right now but
+ /// will eventually expand to support COLR and maybe other formats.
+ /// These are stored as raw outlines; rasterization is delayed to
+ /// renderers. The outlines have been validated.
+ glyph: Glyph,
+
+ /// Authored metrics for the glyph's design coordinate space.
+ design: DesignMetrics,
+
+ /// Unicode cell width requested by the registration.
+ width: request.Width,
+
+ /// Normalized scale, alignment, and padding behavior for rasterization.
+ constraint: Constraint,
+
+ /// Errors that can occur while constructing a glossary entry from a
+ /// register request.
+ pub const InitError = RegisterReq.DecodeError || error{
+ /// The register request is missing a required option or has an invalid
+ /// explicitly-provided option value.
+ InvalidOptions,
+
+ /// The requested payload format is not supported by this glossary.
+ UnsupportedFormat,
+ };
+
+ /// Initialize a glossary entry from a register request.
+ ///
+ /// This validates the request fields needed to construct the entry,
+ /// decodes the base64 glyph payload, and stores the decoded outline. The
+ /// returned entry owns decoded glyph memory and must be released with
+ /// `deinit`.
+ pub fn init(alloc: Allocator, req: RegisterReq) Entry.InitError!Entry {
+ // Validate format
+ const fmt = req.get(.fmt) orelse return error.InvalidOptions;
+ const design: DesignMetrics = .{
+ .units_per_em = req.get(.upm) orelse return error.InvalidOptions,
+ .advance_width = req.get(.aw) orelse return error.InvalidOptions,
+ .line_height = req.get(.lh) orelse return error.InvalidOptions,
+ };
+ if (design.units_per_em == 0 or
+ design.advance_width == 0 or
+ design.line_height == 0) return error.InvalidOptions;
+ const width = req.get(.width) orelse return error.InvalidOptions;
+
+ // Get our constraints
+ const constraint = try constraintFromRegister(req);
+
+ // Decode the payload into some usable glyph format for
+ // future rasterization.
+ const glyph: Glyph = switch (fmt) {
+ .glyf => .{ .glyf = try req.decodeGlyfPayload(alloc) },
+ .colrv0, .colrv1 => return error.UnsupportedFormat,
+ };
+
+ // No more errors, since we never do glyph cleanup above.
+ errdefer comptime unreachable;
+
+ return .{
+ .glyph = glyph,
+ .design = design,
+ .width = width,
+ .constraint = constraint,
+ };
+ }
+
+ /// Release memory owned by this entry.
+ pub fn deinit(self: *Entry, alloc: Allocator) void {
+ switch (self.glyph) {
+ .glyf => |*outline| outline.deinit(alloc),
+ }
+ self.* = undefined;
+ }
+
+ /// Return the renderer constraint for a register request.
+ ///
+ /// Glyph Protocol §8.5 defines sizing, alignment, and padding in terms of
+ /// the authored extent and render span. This function is the single
+ /// normalization point for how protocol sizing choices map to the
+ /// renderer-neutral constraint stored here.
+ fn constraintFromRegister(
+ req: RegisterReq,
+ ) error{InvalidOptions}!Constraint {
+ // Register.get applies the Glyph Protocol §6.1 defaults when options
+ // are omitted: size=height, align=center,center, and pad=0,0,0,0.
+ const size = req.get(.size) orelse return error.InvalidOptions;
+ const alignment = req.get(.@"align") orelse return error.InvalidOptions;
+ const pad = req.get(.pad) orelse return error.InvalidOptions;
+
+ return .{
+ .size = switch (size) {
+ // The rasterizer's base transform already maps the design em
+ // to the cell height. That is the closest existing behavior to
+ // the protocol's default height-driven mode.
+ .height => .none,
+ // There is no width-driven, aspect-preserving constraint mode
+ // today. Leave the base transform intact rather than forcing a
+ // fit/contain policy that would unexpectedly prevent overflow.
+ .advance => .none,
+ // Constraint.cover currently scales preserving aspect ratio to
+ // the available bounds, which is the best existing match for
+ // the protocol's contain mode.
+ .contain => .cover,
+ // There is no true protocol-cover equivalent that chooses the
+ // larger axis scale, so use the nearest named renderer policy.
+ .cover => .cover,
+ .stretch => .stretch,
+ },
+ .align_horizontal = switch (alignment.horizontal) {
+ .start => .start,
+ .center => .center,
+ .end => .end,
+ },
+ .align_vertical = switch (alignment.vertical) {
+ .start => .start,
+ .center => .center,
+ .end => .end,
+ // The current constraint API has no baseline alignment mode.
+ // Start is the closest stable default because the glyf
+ // rasterizer's coordinate model already treats y=0 as the
+ // baseline/bottom before constraints are applied.
+ .baseline => .start,
+ },
+ .pad_top = pad.top,
+ .pad_right = pad.right,
+ .pad_bottom = pad.bottom,
+ .pad_left = pad.left,
+ };
+ }
+};
+
+/// Return true if `cp` is in one of the Unicode Private Use Areas.
+fn isPrivateUse(cp: u21) bool {
+ return (cp >= 0xE000 and cp <= 0xF8FF) or
+ (cp >= 0xF0000 and cp <= 0xFFFFD) or
+ (cp >= 0x100000 and cp <= 0x10FFFD);
+}
+
+fn testParseRegister(alloc: Allocator, data: []const u8) !RegisterReq {
+ const raw = try alloc.dupe(u8, data);
+ errdefer alloc.free(raw);
+
+ const req = try request.Request.parse(alloc, raw);
+ switch (req) {
+ .register => |reg| return reg,
+ else => unreachable,
+ }
+}
+
+// Base64-encoded glyf payload from the "glyf: decode triangle" test in
+// font/opentype/glyf.zig. This is a real simple-glyph record with one contour
+// and three on-curve points.
+const test_triangle_glyf_payload = "AAEAZABkA4QDhAACAAABAQEB9P5wAyADhPzgAAA=";
+
+fn testRegisterReq(alloc: Allocator, cp: u21) !RegisterReq {
+ const data = try std.fmt.allocPrint(
+ alloc,
+ "r;cp={x};upm=2048;aw=1024;lh=1536;width=2;size=stretch;align=end,start;pad=0.1,0.2,0.3,0.4;{s}",
+ .{ cp, test_triangle_glyf_payload },
+ );
+ errdefer alloc.free(data);
+
+ const req = try request.Request.parse(alloc, data);
+ switch (req) {
+ .register => |reg| return reg,
+ else => unreachable,
+ }
+}
+
+fn testRegisterEntry(alloc: Allocator, cp: u21) !Entry {
+ const req = try testRegisterReq(alloc, cp);
+ defer alloc.free(req.raw);
+ return try Entry.init(alloc, req);
+}
+
+test "Entry init decodes glyf payload and applies register fields" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const req = try testRegisterReq(alloc, 0xE000);
+ defer alloc.free(req.raw);
+
+ var entry = try Entry.init(alloc, req);
+ defer entry.deinit(alloc);
+
+ try testing.expectEqual(@as(u32, 2048), entry.design.units_per_em);
+ try testing.expectEqual(@as(u32, 1024), entry.design.advance_width);
+ try testing.expectEqual(@as(u32, 1536), entry.design.line_height);
+ try testing.expectEqual(request.Width.wide, entry.width);
+ try testing.expectEqual(Constraint.Size.stretch, entry.constraint.size);
+ try testing.expectEqual(Constraint.Align.end, entry.constraint.align_horizontal);
+ try testing.expectEqual(Constraint.Align.start, entry.constraint.align_vertical);
+ try testing.expectEqual(@as(f64, 0.1), entry.constraint.pad_top);
+ try testing.expectEqual(@as(f64, 0.2), entry.constraint.pad_right);
+ try testing.expectEqual(@as(f64, 0.3), entry.constraint.pad_bottom);
+ try testing.expectEqual(@as(f64, 0.4), entry.constraint.pad_left);
+
+ try testing.expectEqual(@as(usize, 3), entry.glyph.glyf.points.len);
+ try testing.expectEqual(@as(usize, 1), entry.glyph.glyf.contours.len);
+}
+
+test "Entry init rejects invalid register payload" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const req = try testParseRegister(alloc, "r;cp=e000;%%%not-base64%%%");
+ defer alloc.free(req.raw);
+
+ try testing.expectError(error.MalformedPayload, Entry.init(alloc, req));
+}
+
+test "Glossary register overwrites and moves entry to newest position" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ try glossary.register(alloc, 0xE000, try testRegisterEntry(alloc, 0xE000));
+ try glossary.register(alloc, 0xE001, try testRegisterEntry(alloc, 0xE001));
+ try glossary.register(alloc, 0xE000, try testRegisterEntry(alloc, 0xE000));
+
+ try testing.expectEqual(@as(usize, 2), glossary.entries.count());
+ try testing.expectEqual(@as(u21, 0xE001), glossary.entries.keys()[0]);
+ try testing.expectEqual(@as(u21, 0xE000), glossary.entries.keys()[1]);
+}
+
+test "Glossary register evicts oldest entry" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ for (0..max_entries + 1) |i| {
+ const cp: u21 = @intCast(0xE000 + i);
+ try glossary.register(alloc, cp, try testRegisterEntry(alloc, cp));
+ }
+
+ try testing.expectEqual(@as(usize, max_entries), glossary.entries.count());
+ try testing.expect(!glossary.entries.contains(0xE000));
+ try testing.expect(glossary.entries.contains(0xE001));
+ try testing.expect(glossary.entries.contains(0xE000 + max_entries));
+}
+
+test "Glossary register rejects non-PUA codepoint" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var entry = try testRegisterEntry(alloc, 0xE000);
+ defer entry.deinit(alloc);
+ try testing.expectError(error.OutOfNamespace, glossary.register(alloc, 'A', entry));
+}
+
+test "Glossary delete removes one PUA slot and ignores empty PUA slot" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ try glossary.register(alloc, 0xE000, try testRegisterEntry(alloc, 0xE000));
+ try glossary.register(alloc, 0xE001, try testRegisterEntry(alloc, 0xE001));
+
+ try glossary.delete(alloc, 0xE000);
+ try testing.expectEqual(@as(usize, 1), glossary.entries.count());
+ try testing.expect(!glossary.contains(0xE000));
+ try testing.expect(glossary.contains(0xE001));
+
+ try glossary.delete(alloc, 0xE000);
+ try testing.expectEqual(@as(usize, 1), glossary.entries.count());
+ try testing.expect(glossary.contains(0xE001));
+}
+
+test "Glossary delete rejects non-PUA codepoint" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ try glossary.register(alloc, 0xE000, try testRegisterEntry(alloc, 0xE000));
+ try testing.expectError(error.OutOfNamespace, glossary.delete(alloc, 'A'));
+ try testing.expectEqual(@as(usize, 1), glossary.entries.count());
+ try testing.expect(glossary.contains(0xE000));
+}
+
+test "Glossary clearAndFree removes all slots and remains reusable" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ try glossary.register(alloc, 0xE000, try testRegisterEntry(alloc, 0xE000));
+ try glossary.register(alloc, 0xE001, try testRegisterEntry(alloc, 0xE001));
+
+ glossary.clearAndFree(alloc);
+ try testing.expectEqual(@as(usize, 0), glossary.entries.count());
+ try testing.expect(!glossary.contains(0xE000));
+ try testing.expect(!glossary.contains(0xE001));
+
+ try glossary.register(alloc, 0xE002, try testRegisterEntry(alloc, 0xE002));
+ try testing.expectEqual(@as(usize, 1), glossary.entries.count());
+ try testing.expect(glossary.contains(0xE002));
+}
+
+test "Glossary contains reports registered slots" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ try testing.expect(!glossary.contains(0xE000));
+
+ try glossary.register(alloc, 0xE000, try testRegisterEntry(alloc, 0xE000));
+ try testing.expect(glossary.contains(0xE000));
+ try testing.expect(!glossary.contains(0xE001));
+}
diff --git a/vendor/libghostty-vt/src/terminal/apc/glyph/execute.zig b/vendor/libghostty-vt/src/terminal/apc/glyph/execute.zig
new file mode 100644
index 00000000..059029a6
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/apc/glyph/execute.zig
@@ -0,0 +1,367 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const request = @import("request.zig");
+const response = @import("response.zig");
+const Glossary = @import("Glossary.zig");
+const Request = request.Request;
+const Response = response.Response;
+
+const log = std.log.scoped(.glyph);
+
+/// Payload formats we support. Hardcoded because the support is
+/// fixed.
+pub const supported_formats: response.Response.Support.Formats = .{
+ .glyf = true,
+};
+
+/// Execute a Glyph protocol request against the given state.
+///
+/// This will never fail, but the response may indiciate an error and
+/// the terminal state may not be updated to reflect the command. This will
+/// never put the terminal in a corrupt or non-recoverable state.
+///
+/// For example, allocation errors can happen, but they're wrapped up in
+/// an out of memory response.
+///
+/// Query responses only report glossary coverage. Callers that can determine
+/// system font coverage must update the returned query response before sending
+/// it to the client.
+pub fn execute(
+ alloc: Allocator,
+ glossary: *Glossary,
+ req: *const Request,
+) ?Response {
+ log.debug("executing glyph protocol request: {t}", .{req.*});
+ return switch (req.*) {
+ .support => .{ .support = .{ .fmt = supported_formats } },
+ .query => |qry| query(glossary, qry),
+ .register => |reg| register(alloc, glossary, reg),
+ .clear => |clr| clear(alloc, glossary, clr),
+ };
+}
+
+fn query(
+ glossary: *Glossary,
+ qry: Request.Query,
+) ?Response {
+ const cp = qry.get(.cp) orelse return null;
+ return .{ .query = .{
+ .cp = cp,
+ .status = .{
+ .glossary = glossary.contains(cp),
+ },
+ } };
+}
+
+fn register(
+ alloc: Allocator,
+ glossary: *Glossary,
+ reg: Request.Register,
+) ?Response {
+ const reply = reg.get(.reply) orelse .all;
+ const cp = registerFallible(alloc, glossary, reg) catch |err| return switch (reply) {
+ .none => null,
+ .all, .failures => .{ .register = .{
+ .cp = reg.get(.cp) orelse 0,
+ .status = .err,
+ .reason = switch (err) {
+ error.OutOfMemory => .{ .other = "out_of_memory" },
+ error.OutOfNamespace => .out_of_namespace,
+ error.PayloadTooLarge => .payload_too_large,
+ error.MalformedPayload => .malformed_payload,
+ error.CompositeUnsupported => .composite_unsupported,
+ error.HintingUnsupported => .hinting_unsupported,
+ error.InvalidOptions,
+ error.UnsupportedFormat,
+ => .malformed_payload,
+ },
+ } },
+ };
+
+ return switch (reply) {
+ .none, .failures => null,
+ .all => .{ .register = .{ .cp = cp } },
+ };
+}
+
+fn registerFallible(
+ alloc: Allocator,
+ glossary: *Glossary,
+ reg: Request.Register,
+) (Glossary.Entry.InitError || Glossary.RegisterError)!u21 {
+ const cp = reg.get(.cp) orelse
+ return error.MalformedPayload;
+
+ var entry = try Glossary.Entry.init(alloc, reg);
+ errdefer entry.deinit(alloc);
+
+ try glossary.register(alloc, cp, entry);
+ return cp;
+}
+
+fn clear(
+ alloc: Allocator,
+ glossary: *Glossary,
+ clr: Request.Clear,
+) ?Response {
+ if (clr.get(.cp)) |cp| {
+ glossary.delete(alloc, cp) catch |err| return .{ .clear = .{
+ .status = .err,
+ .reason = switch (err) {
+ error.OutOfNamespace => "out_of_namespace",
+ },
+ } };
+ } else if (clr.has(.cp)) {
+ return .{ .clear = .{
+ .status = .err,
+ .reason = "malformed_payload",
+ } };
+ } else {
+ glossary.clearAndFree(alloc);
+ }
+
+ return .{ .clear = .{} };
+}
+
+fn testParse(alloc: Allocator, data: []const u8) !Request {
+ var parser = request.CommandParser.init(alloc, 1024 * 1024);
+ defer parser.deinit();
+ for (data) |byte| try parser.feed(byte);
+ return try parser.complete(alloc);
+}
+
+fn testExecute(alloc: Allocator, glossary: *Glossary, req: *const Request) ?Response {
+ return execute(alloc, glossary, req);
+}
+
+test "execute register stores glyph and returns success" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var req = try testParse(alloc, "r;cp=e0a0;AAAAAAAAAAAAAA==");
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{
+ .register = .{ .cp = 0xE0A0 },
+ }, testExecute(alloc, &glossary, &req).?);
+ try testing.expect(glossary.contains(0xE0A0));
+}
+
+test "execute register reply failures suppresses success" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var req = try testParse(alloc, "r;cp=e0a0;reply=2;AAAAAAAAAAAAAA==");
+ defer req.deinit(alloc);
+
+ try testing.expect(testExecute(alloc, &glossary, &req) == null);
+ try testing.expect(glossary.contains(0xE0A0));
+}
+
+test "execute register reply none suppresses failure" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var req = try testParse(alloc, "r;cp=41;reply=0;%%%not-base64%%%");
+ defer req.deinit(alloc);
+
+ try testing.expect(testExecute(alloc, &glossary, &req) == null);
+ try testing.expect(!glossary.contains('A'));
+}
+
+test "execute register rejects non-PUA" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var req = try testParse(alloc, "r;cp=41;AAAAAAAAAAAAAA==");
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{
+ .register = .{
+ .cp = 'A',
+ .status = .err,
+ .reason = .out_of_namespace,
+ },
+ }, testExecute(alloc, &glossary, &req).?);
+ try testing.expect(!glossary.contains('A'));
+}
+
+test "execute register reports malformed payload" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var req = try testParse(alloc, "r;cp=e0a0;%%%not-base64%%%");
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{
+ .register = .{
+ .cp = 0xE0A0,
+ .status = .err,
+ .reason = .malformed_payload,
+ },
+ }, testExecute(alloc, &glossary, &req).?);
+ try testing.expect(!glossary.contains(0xE0A0));
+}
+
+test "execute clear removes all glyphs" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var reg1 = try testParse(alloc, "r;cp=e0a0;AAAAAAAAAAAAAA==");
+ defer reg1.deinit(alloc);
+ _ = testExecute(alloc, &glossary, &reg1);
+
+ var reg2 = try testParse(alloc, "r;cp=e0a1;AAAAAAAAAAAAAA==");
+ defer reg2.deinit(alloc);
+ _ = testExecute(alloc, &glossary, &reg2);
+
+ var req = try testParse(alloc, "c");
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{ .clear = .{} }, testExecute(alloc, &glossary, &req).?);
+ try testing.expect(!glossary.contains(0xE0A0));
+ try testing.expect(!glossary.contains(0xE0A1));
+}
+
+test "execute clear removes one glyph" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var reg1 = try testParse(alloc, "r;cp=e0a0;AAAAAAAAAAAAAA==");
+ defer reg1.deinit(alloc);
+ _ = testExecute(alloc, &glossary, &reg1);
+
+ var reg2 = try testParse(alloc, "r;cp=e0a1;AAAAAAAAAAAAAA==");
+ defer reg2.deinit(alloc);
+ _ = testExecute(alloc, &glossary, &reg2);
+
+ var req = try testParse(alloc, "c;cp=e0a0");
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{ .clear = .{} }, testExecute(alloc, &glossary, &req).?);
+ try testing.expect(!glossary.contains(0xE0A0));
+ try testing.expect(glossary.contains(0xE0A1));
+}
+
+test "execute clear rejects non-PUA" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var req = try testParse(alloc, "c;cp=41");
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{
+ .clear = .{
+ .status = .err,
+ .reason = "out_of_namespace",
+ },
+ }, testExecute(alloc, &glossary, &req).?);
+}
+
+test "execute clear rejects malformed cp without clearing glossary" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var reg1 = try testParse(alloc, "r;cp=e0a0;AAAAAAAAAAAAAA==");
+ defer reg1.deinit(alloc);
+ _ = testExecute(alloc, &glossary, &reg1);
+
+ var reg2 = try testParse(alloc, "r;cp=e0a1;AAAAAAAAAAAAAA==");
+ defer reg2.deinit(alloc);
+ _ = testExecute(alloc, &glossary, &reg2);
+
+ for ([_][]const u8{ "c;cp=zz", "c;cp=", "c;cp=200000" }) |data| {
+ var req = try testParse(alloc, data);
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{
+ .clear = .{
+ .status = .err,
+ .reason = "malformed_payload",
+ },
+ }, testExecute(alloc, &glossary, &req).?);
+ try testing.expect(glossary.contains(0xE0A0));
+ try testing.expect(glossary.contains(0xE0A1));
+ }
+}
+
+test "execute query reports no coverage" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var req = try testParse(alloc, "q;cp=e0a0");
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{
+ .query = .{
+ .cp = 0xE0A0,
+ .status = .{},
+ },
+ }, testExecute(alloc, &glossary, &req).?);
+}
+
+test "execute query reports glossary coverage" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var reg = try testParse(alloc, "r;cp=e0a0;AAAAAAAAAAAAAA==");
+ defer reg.deinit(alloc);
+ _ = testExecute(alloc, &glossary, &reg);
+
+ var req = try testParse(alloc, "q;cp=e0a0");
+ defer req.deinit(alloc);
+
+ try testing.expectEqual(Response{
+ .query = .{
+ .cp = 0xE0A0,
+ .status = .{ .glossary = true },
+ },
+ }, testExecute(alloc, &glossary, &req).?);
+}
+
+test "execute query without cp returns no response" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var glossary: Glossary = .empty;
+ defer glossary.deinit(alloc);
+
+ var req = try testParse(alloc, "q;foo=bar");
+ defer req.deinit(alloc);
+
+ try testing.expect(testExecute(alloc, &glossary, &req) == null);
+}
diff --git a/vendor/libghostty-vt/src/terminal/apc/glyph/request.zig b/vendor/libghostty-vt/src/terminal/apc/glyph/request.zig
new file mode 100644
index 00000000..71a2dd4b
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/apc/glyph/request.zig
@@ -0,0 +1,855 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const Glyf = @import("../../../font/opentype/glyf.zig").Glyf;
+
+/// Maximum decoded glyph payload size accepted by the protocol.
+/// This is documented in the spec.
+const max_payload_size = 64 * 1024; // 64 KiB
+
+/// 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);
+ }
+
+ /// Append a slice of APC payload bytes to the buffered command.
+ /// Equivalent to calling feed for each byte, but appends in bulk.
+ pub fn feedSlice(self: *CommandParser, bytes: []const u8) Allocator.Error!void {
+ if (self.data.items.len + bytes.len > self.max_bytes) return error.OutOfMemory;
+ try self.data.appendSlice(self.alloc, bytes);
+ }
+
+ /// 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 {
+ if (raw.len < 2) return null;
+ if (raw[0] != 'r') return null;
+ if (raw[1] != ';') return null;
+ const payload_idx = std.mem.lastIndexOfScalar(u8, raw, ';') orelse return null;
+ if (payload_idx <= 1) return null;
+
+ 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 ..];
+ }
+
+ /// Errors that can occur while decoding a register glyph payload.
+ pub const DecodeError = Allocator.Error || error{
+ /// The decoded payload exceeds the protocol limit.
+ PayloadTooLarge,
+
+ /// The payload could not be decoded or parsed as the declared format.
+ MalformedPayload,
+
+ /// The glyf payload is composite, which the protocol forbids.
+ CompositeUnsupported,
+
+ /// The glyf payload contains hinting instructions, which the
+ /// protocol forbids.
+ HintingUnsupported,
+ };
+
+ /// Decode this request's base64 glyf payload into an owned outline.
+ pub fn decodeGlyfPayload(self: Register, alloc: Allocator) DecodeError!Glyf.Outline {
+ // Prep base64 decoding, initial validation.
+ const Decoder = std.base64.standard.Decoder;
+ const payload_bytes = self.payload();
+ const size = Decoder.calcSizeForSlice(payload_bytes) catch
+ return error.MalformedPayload;
+ if (size > max_payload_size) return error.PayloadTooLarge;
+
+ // Max payload size is reasonable for stack and its likely
+ // we'll have stack space. We don't use much stack space in
+ // the future function calls either, so try a stack allocator
+ // here and fallback to heap as necessary.
+ var data_stack = std.heap.stackFallback(
+ max_payload_size,
+ alloc,
+ );
+ const data_alloc = data_stack.get();
+ const data = try data_alloc.alloc(u8, size);
+ defer data_alloc.free(data);
+
+ // Base64 decode
+ Decoder.decode(data, payload_bytes) catch
+ return error.MalformedPayload;
+
+ // Glyf.Entry borrows from `data`, but only for the duration of the
+ // decode call below. Glyf.Entry.decode returns an owned Outline, so
+ // it is safe to free `data` before returning that outline.
+ const glyf_entry = Glyf.Entry.init(data) catch return error.MalformedPayload;
+ return glyf_entry.decode(alloc) catch |err| switch (err) {
+ error.OutOfMemory => error.OutOfMemory,
+ // Unsupported fields
+ error.CompositeNotSupported => error.CompositeUnsupported,
+ error.InstructionsNotSupported => error.HintingUnsupported,
+ // Various semantic issues
+ error.EndOfStream,
+ error.EndPointsOutOfOrder,
+ error.TooManyPoints,
+ error.CoordinateOverflow,
+ => error.MalformedPayload,
+ };
+ }
+
+ /// 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,
+ };
+ }
+
+ /// Return whether the option is present in the raw option string,
+ /// independent of whether its value can be decoded.
+ pub fn present(comptime self: Option, raw: []const u8) bool {
+ return optionValue(raw, self.key()) != null;
+ }
+ };
+
+ /// Lazily decode a clear option on demand.
+ pub fn get(self: Clear, comptime option: Option) ?option.Type() {
+ return option.read(self.rawOptions());
+ }
+
+ /// Return whether a clear option was provided, even if malformed.
+ pub fn has(self: Clear, comptime option: Option) bool {
+ return option.present(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 = Register.init(raw) orelse return error.InvalidFormat },
+ '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 command rejects missing payload separator" {
+ const testing = std.testing;
+
+ for ([_][]const u8{ "r", "r;cp=e0a0", "r;foo" }) |data| {
+ try testing.expectError(
+ error.InvalidFormat,
+ testParse(testing.allocator, data),
+ );
+ }
+}
+
+test "register decodes glyf payload" {
+ const testing = std.testing;
+
+ var cmd = try testParse(testing.allocator, "r;cp=e0a0;AAAAAAAAAAAAAA==");
+ defer cmd.deinit(testing.allocator);
+
+ var outline = try cmd.register.decodeGlyfPayload(testing.allocator);
+ defer outline.deinit(testing.allocator);
+
+ try testing.expectEqual(@as(usize, 0), outline.points.len);
+ try testing.expectEqual(@as(usize, 0), outline.contours.len);
+}
+
+test "register rejects malformed glyf payload" {
+ const testing = std.testing;
+
+ var cmd = try testParse(testing.allocator, "r;cp=e0a0;%%%not-base64%%%");
+ defer cmd.deinit(testing.allocator);
+
+ try testing.expectError(error.MalformedPayload, cmd.register.decodeGlyfPayload(testing.allocator));
+}
+
+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.expect(cmd.clear.has(.cp));
+ try testing.expectEqual(@as(u21, 0xE0A0), cmd.clear.get(.cp).?);
+}
+
+test "clear command tracks malformed cp presence" {
+ const testing = std.testing;
+
+ for ([_][]const u8{ "c;cp=zz", "c;cp=", "c;cp=200000" }) |data| {
+ var cmd = try testParse(testing.allocator, data);
+ defer cmd.deinit(testing.allocator);
+
+ try testing.expect(cmd == .clear);
+ try testing.expect(cmd.clear.has(.cp));
+ try testing.expect(cmd.clear.get(.cp) == null);
+ }
+}
+
+test "invalid command" {
+ const testing = std.testing;
+
+ try testing.expectError(
+ error.InvalidFormat,
+ testParse(testing.allocator, "x"),
+ );
+}
diff --git a/vendor/libghostty-vt/src/terminal/apc/glyph/response.zig b/vendor/libghostty-vt/src/terminal/apc/glyph/response.zig
new file mode 100644
index 00000000..9e40f0ea
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/apc/glyph/response.zig
@@ -0,0 +1,346 @@
+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) {
+ /// Recommended fixed buffer size for formatting a Glyph Protocol response.
+ ///
+ /// Glyph Protocol responses contain only framing plus bounded scalar fields:
+ /// a u21 codepoint as hex, a decimal u8 status, a small fixed set of
+ /// supported format names, coverage names, and the reason names produced by
+ /// the executor. 1024 bytes is therefore far above the longest response we
+ /// can emit today, while still being small enough for stack allocation in
+ /// stream handlers. If callers construct responses with arbitrary `.other`
+ /// or clear reason strings, they must ensure those strings fit or handle the
+ /// writer error from `formatWire`.
+ pub const max_wire_bytes = 1024;
+
+ /// 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());
+}