diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:28:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:29:28 +0300 |
| commit | 4a3302d19a2baa3a33e910e0088c24e780905b90 (patch) | |
| tree | dcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/terminal/apc | |
| parent | 550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (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.zig | 164 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/apc/glyph/AGENTS.md | 9 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/apc/glyph/Glossary.zig | 444 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/apc/glyph/execute.zig | 367 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/apc/glyph/request.zig | 855 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/apc/glyph/response.zig | 346 |
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, ®1); + + var reg2 = try testParse(alloc, "r;cp=e0a1;AAAAAAAAAAAAAA=="); + defer reg2.deinit(alloc); + _ = testExecute(alloc, &glossary, ®2); + + 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, ®1); + + var reg2 = try testParse(alloc, "r;cp=e0a1;AAAAAAAAAAAAAA=="); + defer reg2.deinit(alloc); + _ = testExecute(alloc, &glossary, ®2); + + 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, ®1); + + var reg2 = try testParse(alloc, "r;cp=e0a1;AAAAAAAAAAAAAA=="); + defer reg2.deinit(alloc); + _ = testExecute(alloc, &glossary, ®2); + + 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, ®); + + 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()); +} |
