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 ; ; * 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()); }