diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/osc | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/osc')
9 files changed, 1435 insertions, 356 deletions
diff --git a/vendor/libghostty-vt/src/terminal/osc/kitty_metadata.zig b/vendor/libghostty-vt/src/terminal/osc/kitty_metadata.zig new file mode 100644 index 00000000..469c97a0 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/osc/kitty_metadata.zig @@ -0,0 +1,94 @@ +//! Helpers for parsing metadata shared by Kitty OSC protocols. +//! +//! Kitty OSC 99 and OSC 5522 encode metadata as colon-separated `key=value` +//! fields. The iterator in this module lazily searches that metadata for one +//! key, preserving the order of repeated values without allocating. +//! +//! Parsing is intentionally tolerant. Whitespace around keys and values is +//! trimmed, while malformed fields, non-matching keys, and invalid values are +//! skipped. Returned values are slices of the original metadata and remain +//! valid only as long as that input remains valid. + +const std = @import("std"); + +/// Return an iterator over values whose key exactly matches `key`. +/// +/// If `valid_value_characters` is non-null, every byte in a returned value must +/// appear in that character set. Passing null disables value validation. Both +/// arguments are comptime-known so each protocol can specialize the iterator +/// for its metadata grammar without storing a key or validator at runtime. +pub fn ValueIterator( + comptime key: []const u8, + comptime valid_value_characters: ?[]const u8, +) type { + return struct { + const Self = @This(); + + metadata: []const u8, + pos: usize, + + /// Initialize an iterator borrowing `metadata`. + pub fn init(metadata: []const u8) Self { + return .{ + .metadata = metadata, + .pos = 0, + }; + } + + /// Return the next valid matching value, or null when none remain. + /// The returned slice borrows the metadata passed to `init`. + pub fn next(self: *Self) ?[]const u8 { + while (self.pos < self.metadata.len) { + const end = std.mem.indexOfScalarPos( + u8, + self.metadata, + self.pos, + ':', + ) orelse self.metadata.len; + const field = self.metadata[self.pos..end]; + self.pos = if (end < self.metadata.len) end + 1 else end; + + const equals = std.mem.indexOfScalar(u8, field, '=') orelse + continue; + const field_key = std.mem.trim( + u8, + field[0..equals], + &std.ascii.whitespace, + ); + if (!std.mem.eql(u8, field_key, key)) continue; + + const value = std.mem.trim( + u8, + field[equals + 1 ..], + &std.ascii.whitespace, + ); + if (valid_value_characters) |valid| { + if (std.mem.indexOfNone(u8, value, valid) != null) continue; + } + + return value; + } + + return null; + } + }; +} + +test "ValueIterator skips malformed and prefix-matching keys" { + const testing = std.testing; + var it: ValueIterator("id", null) = .init( + "id-extra=wrong:id: id = first :id=second", + ); + + try testing.expectEqualStrings("first", it.next().?); + try testing.expectEqualStrings("second", it.next().?); + try testing.expect(it.next() == null); +} + +test "ValueIterator skips values containing disallowed characters" { + const testing = std.testing; + var it: ValueIterator("i", "abc") = .init("i=a?:i=abc"); + + try testing.expectEqualStrings("abc", it.next().?); + try testing.expect(it.next() == null); +} diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers.zig b/vendor/libghostty-vt/src/terminal/osc/parsers.zig index 8cb91fb9..148bfa33 100644 --- a/vendor/libghostty-vt/src/terminal/osc/parsers.zig +++ b/vendor/libghostty-vt/src/terminal/osc/parsers.zig @@ -10,6 +10,7 @@ pub const iterm2 = @import("parsers/iterm2.zig"); pub const kitty_clipboard_protocol = @import("parsers/kitty_clipboard_protocol.zig"); pub const kitty_color = @import("parsers/kitty_color.zig"); pub const kitty_dnd_protocol = @import("parsers/kitty_dnd_protocol.zig"); +pub const kitty_desktop_notification = @import("parsers/kitty_desktop_notification.zig"); pub const kitty_text_sizing = @import("parsers/kitty_text_sizing.zig"); pub const mouse_shape = @import("parsers/mouse_shape.zig"); pub const osc9 = @import("parsers/osc9.zig"); diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig index 8d149f97..01588771 100644 --- a/vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig +++ b/vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig @@ -6,13 +6,13 @@ const Parser = @import("../../osc.zig").Parser; const Command = @import("../../osc.zig").Command; /// Parse OSC 52 -pub fn parse(parser: *Parser, _: ?u8) ?*Command { +pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { assert(parser.state == .@"52"); const cap = if (parser.capture) |*c| c else { parser.state = .invalid; return null; }; - cap.writer.writeByte(0) catch { + cap.writeByte(0) catch { parser.state = .invalid; return null; }; @@ -26,6 +26,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { .clipboard_contents = .{ .kind = 'c', .data = data[1 .. data.len - 1 :0], + .terminator = .init(terminator_ch), }, }; } else { @@ -41,6 +42,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { .clipboard_contents = .{ .kind = data[0], .data = data[2 .. data.len - 1 :0], + .terminator = .init(terminator_ch), }, }; } @@ -59,6 +61,20 @@ test "OSC 52: get/set clipboard" { try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 's'); try testing.expectEqualStrings("?", cmd.clipboard_contents.data); + try testing.expectEqual(.st, cmd.clipboard_contents.terminator); +} + +test "OSC 52: get clipboard with BEL terminator" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "52;c;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end(0x07).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expectEqual(.bel, cmd.clipboard_contents.terminator); } test "OSC 52: get/set clipboard (optional parameter)" { diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/color.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/color.zig index 547c98ea..d8b89f41 100644 --- a/vendor/libghostty-vt/src/terminal/osc/parsers/color.zig +++ b/vendor/libghostty-vt/src/terminal/osc/parsers/color.zig @@ -6,6 +6,7 @@ const SpecialColor = @import("../../color.zig").Special; const RGB = @import("../../color.zig").RGB; const Parser = @import("../../osc.zig").Parser; const Command = @import("../../osc.zig").Command; +const SegmentedList = @import("../../../datastruct/segmented_list.zig").SegmentedList; const log = std.log.scoped(.osc_color); @@ -182,19 +183,19 @@ fn parseGetSetAnsiColor( // Parse the color. const target: Target = switch (op) { // OSC5 maps directly to the Special enum. - .osc_5 => .{ .special = std.meta.intToEnum( + .osc_5 => .{ .special = std.enums.fromInt( SpecialColor, std.math.cast(u3, color) orelse return result, - ) catch return result }, + ) orelse return result }, // OSC4 maps 0-255 to palette, 256-259 to special offset // by the palette count. .osc_4 => if (std.math.cast(u8, color)) |idx| .{ .palette = idx, - } else .{ .special = std.meta.intToEnum( + } else .{ .special = std.enums.fromInt( SpecialColor, std.math.cast(u3, color - 256) orelse return result, - ) catch return result }, + ) orelse return result }, else => comptime unreachable, }; @@ -255,19 +256,19 @@ fn parseResetAnsiColor( // Parse the color. const target: Target = switch (op) { // OSC105 maps directly to the Special enum. - .osc_105 => .{ .special = std.meta.intToEnum( + .osc_105 => .{ .special = std.enums.fromInt( SpecialColor, std.math.cast(u3, color) orelse continue, - ) catch continue }, + ) orelse continue }, // OSC104 maps 0-255 to palette, 256-259 to special offset // by the palette count. .osc_104 => if (std.math.cast(u8, color)) |idx| .{ .palette = idx, - } else .{ .special = std.meta.intToEnum( + } else .{ .special = std.enums.fromInt( SpecialColor, std.math.cast(u3, color - 256) orelse continue, - ) catch continue }, + ) orelse continue }, else => comptime unreachable, }; @@ -329,7 +330,7 @@ fn parseResetDynamicColor( /// The exact prealloc value is chosen arbitrarily assuming most /// color ops have very few. If we can get empirical data on more /// typical values we can switch to that. -pub const List = std.SegmentedList( +pub const List = SegmentedList( Request, 2, ); @@ -450,7 +451,7 @@ test "OSC 4:" { // Test every special color for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| { - const special = try std.meta.intToEnum(SpecialColor, i); + const special = std.enums.fromInt(SpecialColor, i) orelse return error.InvalidEnumValue; // Simple color set // printf '\e]4;256;red\\' @@ -482,7 +483,7 @@ test "OSC 5:" { // Test every special color for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| { - const special = try std.meta.intToEnum(SpecialColor, i); + const special = std.enums.fromInt(SpecialColor, i) orelse return error.InvalidEnumValue; // Simple color set // printf '\e]4;256;red\\' @@ -592,7 +593,7 @@ test "OSC 104:" { // Test every special color for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| { - const special = try std.meta.intToEnum(SpecialColor, i); + const special = std.enums.fromInt(SpecialColor, i) orelse return error.InvalidEnumValue; // Simple color set // printf '\e]104;256\\' diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_clipboard_protocol.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_clipboard_protocol.zig index 5bd0e254..1bcc7d38 100644 --- a/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_clipboard_protocol.zig +++ b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_clipboard_protocol.zig @@ -10,6 +10,7 @@ const assert = @import("../../../quirks.zig").inlineAssert; const Parser = @import("../../osc.zig").Parser; const Command = @import("../../osc.zig").Command; const Terminator = @import("../../osc.zig").Terminator; +const kitty_metadata = @import("../kitty_metadata.zig"); const encoding = @import("../encoding.zig"); const log = std.log.scoped(.kitty_clipboard_protocol); @@ -22,6 +23,12 @@ pub const OSC = struct { /// The terminator that was used in case we need to send a response. terminator: Terminator, + pub const C = void; + + pub fn cval(_: OSC) C { + return {}; + } + /// Decode an option from the metadata. pub fn readOption(self: OSC, comptime key: Option) ?key.Type() { return key.read(self.metadata); @@ -51,6 +58,7 @@ pub const Status = enum { DATA, DONE, EBUSY, + EFBIG, EINVAL, EIO, ENOSYS, @@ -90,37 +98,8 @@ pub const Option = enum { comptime key: Option, metadata: []const u8, ) ?key.Type() { - const value: []const u8 = value: { - var pos: usize = 0; - while (pos < metadata.len) { - // skip any whitespace - while (pos < metadata.len and std.ascii.isWhitespace(metadata[pos])) pos += 1; - // bail if we are out of metadata - if (pos >= metadata.len) return null; - if (!std.mem.startsWith(u8, metadata[pos..], @tagName(key))) { - // this isn't the key we are looking for, skip to the next option, or bail if - // there is no next option - pos = std.mem.indexOfScalarPos(u8, metadata, pos, ':') orelse return null; - pos += 1; - continue; - } - // skip past the key - pos += @tagName(key).len; - // skip any whitespace - while (pos < metadata.len and std.ascii.isWhitespace(metadata[pos])) pos += 1; - // bail if we are out of metadata - if (pos >= metadata.len) return null; - // a valid option has an '=' - if (metadata[pos] != '=') return null; - // the end of the value is bounded by a ':' or the end of the metadata - const end = std.mem.indexOfScalarPos(u8, metadata, pos, ':') orelse metadata.len; - const start = pos + 1; - // strip any leading or trailing whitespace - break :value std.mem.trim(u8, metadata[start..end], &std.ascii.whitespace); - } - // the key was not found - return null; - }; + var it: kitty_metadata.ValueIterator(@tagName(key), null) = .init(metadata); + const value = it.next() orelse return null; // return the parsed value return switch (key) { diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_desktop_notification.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_desktop_notification.zig new file mode 100644 index 00000000..27fc8670 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_desktop_notification.zig @@ -0,0 +1,1201 @@ +//! Kitty's desktop notification protocol (OSC 99) +//! Specification: https://sw.kovidgoyal.net/kitty/desktop-notifications/ + +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; +const Terminator = @import("../../osc.zig").Terminator; +const kitty_metadata = @import("../kitty_metadata.zig"); +const encoding = @import("../encoding.zig"); + +const log = std.log.scoped(.kitty_desktop_notification); + +pub const MAX_PLAIN_PAYLOAD_BYTES = 2048; +pub const MAX_ENCODED_PAYLOAD_BYTES = 4096; + +pub const OSC = struct { + /// The raw metadata that was received. It can be parsed by using the `readOption` method. + metadata: []const u8, + /// The raw payload. It may be Base64 encoded, check the `e` option. + payload: []const u8, + /// The terminator that was used in case we need to send a response. + terminator: Terminator, + + /// Decode an option from the metadata. + pub fn readOption(self: OSC, comptime key: Option) key.Type() { + return key.read(self.metadata); + } +}; + +pub const Action = packed struct { + focus: bool, + report: bool, + + pub const default: Action = .{ + .focus = true, + .report = false, + }; + + pub fn init(str: []const u8) Action { + return parsePackedStruct(Action, str); + } +}; + +pub const Occasion = enum { + always, + invisible, + unfocused, + + pub const default: Occasion = .always; + + pub fn init(str: []const u8) Occasion { + return std.meta.stringToEnum(Occasion, str) orelse .default; + } +}; + +pub const Payload = enum { + alive, + body, + buttons, + close, + icon, + query, + title, + /// This is a special value to indicate that an unknown payload value was + /// specified and it should be ignored. + unknown, + + pub const default: Payload = .title; + + pub fn init(str: []const u8) Payload { + if (str.len == 1 and str[0] == '?') return .query; + // The string `query` is not allowed, it should be a single question + // mark if you want a query. + if (std.mem.eql(u8, "query", str)) return .unknown; + return std.meta.stringToEnum(Payload, str) orelse .unknown; + } +}; + +pub const Urgency = enum { + low, + normal, + high, + + pub const default: Urgency = .normal; + + pub fn init(str: []const u8) Urgency { + if (str.len != 1) return .default; + return switch (str[0]) { + '0' => .low, + '1' => .normal, + '2' => .high, + else => .default, + }; + } +}; + +pub const Option = enum { + /// What action(s) should be taken when a notification is clicked. + a, + /// Should a notification be sent to the application when the notification + /// is closed? + c, + /// Are we done with the notification, and it is ready to be sent? + d, + /// Is the payload encoded with Base64? + e, + /// The name of the application that is sending the notification. + f, + /// Identifier for icon data. Only used when the payload is icon data. + g, + /// Identifier for the notification. + i, + /// Icon name. + n, + /// When to honor the notification request. + o, + /// Type of the payload. + p, + /// The sound name to play with the notification. + s, + /// The type of the notification. + t, + /// The urgency of the notification. + u, + /// When to auto-close the notification. + w, + + pub fn Type(comptime key: Option) type { + return switch (key) { + .a => Action, + .c => bool, + .d => bool, + .e => bool, + .f => ?[]const u8, + .g => ?[]const u8, + .i => ?[]const u8, + .n => kitty_metadata.ValueIterator( + @tagName(key), + valid_metadata_value_characters, + ), + .o => Occasion, + .p => Payload, + .s => []const u8, + .t => kitty_metadata.ValueIterator( + @tagName(key), + valid_metadata_value_characters, + ), + .u => Urgency, + .w => i32, + }; + } + + pub fn default(comptime key: Option) key.Type() { + return switch (key) { + .a => .default, + .c => false, + .d => true, + .e => false, + .f => null, + .g => null, + .i => null, + .n => unreachable, + .o => .default, + .p => .default, + .s => "system", + .t => unreachable, + .u => .default, + .w => -1, + }; + } + + /// Read the option value from the raw metadata string. + /// + /// Unknown and malformed values are ignored. Optional values return null; + /// all other values return the protocol default. + pub fn read( + comptime key: Option, + metadata: []const u8, + ) key.Type() { + var it: kitty_metadata.ValueIterator( + @tagName(key), + valid_metadata_value_characters, + ) = switch (key) { + .t, .n => return .init(metadata), + else => .init(metadata), + }; + + const value = it.next() orelse return key.default(); + + // return the parsed value, the iterator guarantees that it's a valid + // metadata value + return switch (key) { + .a => .init(value), + .c => parseBool(value) orelse key.default(), + .d => parseBool(value) orelse key.default(), + .e => parseBool(value) orelse key.default(), + .f => value, + .g => parseIdentifier(value), + .i => parseIdentifier(value), + .n => unreachable, + .o => .init(value), + .p => .init(value), + .s => value, + .t => unreachable, + .u => .init(value), + .w => value: { + // Zig's integer parser allows '_', we don't + if (std.mem.indexOfScalar(u8, value, '_')) |_| break :value key.default(); + const tmp = std.fmt.parseInt(i32, value, 10) catch break :value key.default(); + // negative values less than -1 are not allowed + if (tmp < -1) break :value key.default(); + break :value tmp; + }, + }; + } +}; + +/// Parse the protocol's booleans +fn parseBool(str: []const u8) ?bool { + if (str.len != 1) return null; + return switch (str[0]) { + '0' => false, + '1' => true, + else => null, + }; +} + +/// This is similar to the packed struct parser used in the configs. The +/// differences are that a literal `true` or `false` value does not turn on/off +/// all the values, and the negation prefix is `-` not `no-`. +fn parsePackedStruct(comptime T: type, str: []const u8) T { + const info = @typeInfo(T).@"struct"; + comptime assert(info.layout == .@"packed"); + + var result: T = .default; + + // We split each value by "," + var iter = std.mem.splitSequence(u8, str, ","); + loop: while (iter.next()) |raw| { + // Determine the field we're looking for and the value. If the + // field is prefixed with "-" then we set the value to false. + const part, const value = part: { + const negation_prefix = "-"; + const trimmed = std.mem.trim(u8, raw, &std.ascii.whitespace); + if (std.mem.startsWith(u8, trimmed, negation_prefix)) { + break :part .{ trimmed[negation_prefix.len..], false }; + } else { + break :part .{ trimmed, true }; + } + }; + + inline for (info.fields) |field| { + comptime assert(field.type == bool); + if (std.mem.eql(u8, field.name, part)) { + @field(result, field.name) = value; + continue :loop; + } + } + + // No field matched + return .default; + } + + return result; +} + +/// Characters that are valid in a metadata value. Including `=` is technically +/// against the spec but is needed since Base64 encoded values (with padding) +/// are valid for some options. Including `?` is technically against the spec +/// but is needed since it is a valid value for the `p` option. +const valid_metadata_value_characters: []const u8 = valid_identifier_characters ++ "/,(){}[]*&^%$#@!`~=?"; + +/// Characters that are valid in identifiers. +const valid_identifier_characters: []const u8 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+."; + +fn isValidIdentifier(str: []const u8) bool { + return std.mem.indexOfNone(u8, str, valid_identifier_characters) == null; +} + +fn parseIdentifier(str: []const u8) ?[]const u8 { + if (isValidIdentifier(str)) return str; + return null; +} + +pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { + assert(parser.state == .@"99"); + + const cap = if (parser.capture) |*c| c else { + parser.state = .invalid; + return null; + }; + + const data = cap.trailing(); + + const payload_start = std.mem.indexOfScalar(u8, data, ';') orelse { + log.warn("missing semicolon before payload", .{}); + parser.state = .invalid; + return null; + }; + + const metadata = data[0..payload_start]; + const payload = data[payload_start + 1 .. data.len]; + + const max_payload_bytes: usize = if (Option.e.read(metadata)) + MAX_ENCODED_PAYLOAD_BYTES + else + MAX_PLAIN_PAYLOAD_BYTES; + if (payload.len > max_payload_bytes) { + log.warn( + "payload is too large: size={d} max={d}", + .{ payload.len, max_payload_bytes }, + ); + parser.state = .invalid; + return null; + } + + // Payload has to be an escape-code-safe UTF-8 string. + if (!encoding.isSafeUtf8(payload)) { + log.warn("payload is not escape code safe UTF-8", .{}); + parser.state = .invalid; + return null; + } + + parser.command = .{ + .kitty_desktop_notification = .{ + .metadata = metadata, + .payload = payload, + .terminator = .init(terminator_ch), + }, + }; + + return &parser.command; +} + +test "OSC 99: empty metadata and payload" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("", cmd.kitty_desktop_notification.metadata); + try testing.expectEqualStrings("", cmd.kitty_desktop_notification.payload); + try testing.expectEqualDeep(Option.a.default(), cmd.kitty_desktop_notification.readOption(.a)); + try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == false); + try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == true); + try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == false); + try testing.expect(cmd.kitty_desktop_notification.readOption(.f) == null); + try testing.expect(cmd.kitty_desktop_notification.readOption(.g) == null); + try testing.expect(cmd.kitty_desktop_notification.readOption(.i) == null); + { + var it = cmd.kitty_desktop_notification.readOption(.n); + try testing.expect(it.next() == null); + } + try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o)); + try testing.expectEqual(.title, cmd.kitty_desktop_notification.readOption(.p)); + try testing.expectEqualStrings("system", cmd.kitty_desktop_notification.readOption(.s)); + { + var it = cmd.kitty_desktop_notification.readOption(.t); + try testing.expect(it.next() == null); + } + try testing.expectEqual(.normal, cmd.kitty_desktop_notification.readOption(.u)); + try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w)); + try testing.expectEqual(.st, cmd.kitty_desktop_notification.terminator); +} + +test "OSC 99: empty metadata with payload" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;;bobr"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("", cmd.kitty_desktop_notification.metadata); + try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.payload); + try testing.expectEqualDeep(Option.a.default(), cmd.kitty_desktop_notification.readOption(.a)); + try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == false); + try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == true); + try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == false); + try testing.expect(cmd.kitty_desktop_notification.readOption(.f) == null); + try testing.expect(cmd.kitty_desktop_notification.readOption(.g) == null); + try testing.expect(cmd.kitty_desktop_notification.readOption(.i) == null); + { + var it = cmd.kitty_desktop_notification.readOption(.n); + try testing.expect(it.next() == null); + } + try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o)); + try testing.expectEqual(.title, cmd.kitty_desktop_notification.readOption(.p)); + try testing.expectEqualStrings("system", cmd.kitty_desktop_notification.readOption(.s)); + { + var it = cmd.kitty_desktop_notification.readOption(.t); + try testing.expect(it.next() == null); + } + try testing.expectEqual(.normal, cmd.kitty_desktop_notification.readOption(.u)); + try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w)); +} + +test "OSC 99: payload size limits" { + const testing = std.testing; + const cases = [_]struct { + metadata: []const u8, + payload_size: usize, + valid: bool, + }{ + .{ .metadata = "", .payload_size = MAX_PLAIN_PAYLOAD_BYTES, .valid = true }, + .{ .metadata = "", .payload_size = MAX_PLAIN_PAYLOAD_BYTES + 1, .valid = false }, + .{ .metadata = "e=1", .payload_size = MAX_ENCODED_PAYLOAD_BYTES, .valid = true }, + .{ .metadata = "e=1", .payload_size = MAX_ENCODED_PAYLOAD_BYTES + 1, .valid = false }, + }; + + for (cases) |case| { + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + for ("99;") |ch| p.next(ch); + for (case.metadata) |ch| p.next(ch); + p.next(';'); + for (0..case.payload_size) |_| p.next('a'); + + try testing.expectEqual(case.valid, p.end('\x1b') != null); + } +} + +test "OSC 99: unknown prefix does not hide dotted identifier" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "99;invalid=wrong:i=org.ghostty;payload"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expectEqualStrings( + "org.ghostty", + cmd.kitty_desktop_notification.readOption(.i).?, + ); +} + +test "OSC 99: single parameter i" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;i=bobr;payload"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?); + try testing.expectEqualStrings("payload", cmd.kitty_desktop_notification.payload); +} + +test "OSC 99: repeated parameter i" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;i=bobr:i=foobar;payload"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?); + try testing.expectEqualStrings("payload", cmd.kitty_desktop_notification.payload); +} + +test "OSC 99: multiple types" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;t=mail: t = chat : t = alert ;notification"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("notification", cmd.kitty_desktop_notification.payload); + var it = cmd.kitty_desktop_notification.readOption(.t); + try testing.expectEqualStrings("mail", it.next().?); + try testing.expectEqualStrings("chat", it.next().?); + try testing.expectEqualStrings("alert", it.next().?); + try testing.expect(it.next() == null); +} + +test "OSC 99: a 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;a=report,focus;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualDeep(Action{ .report = true, .focus = true }, cmd.kitty_desktop_notification.readOption(.a)); +} + +test "OSC 99: a 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;a=report,-focus;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualDeep(Action{ .report = true, .focus = false }, cmd.kitty_desktop_notification.readOption(.a)); +} + +test "OSC 99: a 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;a=-report,focus;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualDeep(Action{ .report = false, .focus = true }, cmd.kitty_desktop_notification.readOption(.a)); +} + +test "OSC 99: a 4" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;a=-report,-focus;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualDeep(Action{ .report = false, .focus = false }, cmd.kitty_desktop_notification.readOption(.a)); +} + +test "OSC 99: c 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;c=0;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == false); +} + +test "OSC 99: c 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;c=1;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == true); +} + +test "OSC 99: c 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;c=bobr;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == false); +} + +test "OSC 99: d 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;d=0;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == false); +} + +test "OSC 99: d 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;d=1;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == true); +} + +test "OSC 99: d 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;d=bobr;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == true); +} + +test "OSC 99: e 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;e=0;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == false); +} + +test "OSC 99: e 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;e=1;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == true); +} + +test "OSC 99: e 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;e=bobr;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == false); +} + +test "OSC 99: f 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;f=R2hvc3R0eQ==;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_desktop_notification.readOption(.f).?); +} + +test "OSC 99: f 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;c=0:f= R2hvc3R0eQ== ;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_desktop_notification.readOption(.f).?); +} + +test "OSC 99: g 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;c=0:g=7f8a9129-a35d-4e9f-8043-ce2700e15e2c;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("7f8a9129-a35d-4e9f-8043-ce2700e15e2c", cmd.kitty_desktop_notification.readOption(.g).?); +} + +test "OSC 99: g 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;c=0:g=aaa*bbb;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.g) == null); +} + +test "OSC 99: i 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expect(cmd.kitty_desktop_notification.readOption(.i) == null); +} + +test "OSC 99: i 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;i=bobr;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?); +} + +test "OSC 99: i 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;i=;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("", cmd.kitty_desktop_notification.readOption(.i).?); +} + +test "OSC 99: i 4" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;i= :;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("", cmd.kitty_desktop_notification.readOption(.i).?); +} + +test "OSC 99: i 5" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;i= bobr ;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?); +} + +test "OSC 99: i 6" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;i= bobr : i=kurwa ;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?); +} + +test "OSC 99: n 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;n=R2hvc3R0eQ==;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + var it = cmd.kitty_desktop_notification.readOption(.n); + try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?); + try testing.expect(it.next() == null); +} + +test "OSC 99: n 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;n=R2hvc3R0eQ==:n=R2hvc3R0eQ==;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + var it = cmd.kitty_desktop_notification.readOption(.n); + try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?); + try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?); + try testing.expect(it.next() == null); +} + +test "OSC 99: o 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;o= ;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o)); +} + +test "OSC 99: o 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;o=always;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o)); +} + +test "OSC 99: o 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;o=unfocused;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.unfocused, cmd.kitty_desktop_notification.readOption(.o)); +} + +test "OSC 99: o 4" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;o=invisible;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.invisible, cmd.kitty_desktop_notification.readOption(.o)); +} + +test "OSC 99: o 5" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;o=bobr;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o)); +} + +test "OSC 99: p 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=alive;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.alive, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: p 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=body;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.body, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: p 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=buttons;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.buttons, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: p 4" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=close;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.close, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: p 5" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=icon;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.icon, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: p 6" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=?;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.query, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: p 7" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=title;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.title, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: p 8" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=query;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.unknown, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: p 9" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;p=bobr;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.unknown, cmd.kitty_desktop_notification.readOption(.p)); +} + +test "OSC 99: s 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;s=R2hvc3R0eQ==;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_desktop_notification.readOption(.s)); +} + +test "OSC 99: t 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;t=R2hvc3R0eQ==;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + var it = cmd.kitty_desktop_notification.readOption(.t); + try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?); + try testing.expect(it.next() == null); +} + +test "OSC 99: t 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;t=R2hvc3R0eQ==:t=R2hvc3R0eQ==;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + var it = cmd.kitty_desktop_notification.readOption(.t); + try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?); + try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?); + try testing.expect(it.next() == null); +} + +test "OSC 99: u 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;u=0;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.low, cmd.kitty_desktop_notification.readOption(.u)); +} + +test "OSC 99: u 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;u=1;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.normal, cmd.kitty_desktop_notification.readOption(.u)); +} + +test "OSC 99: u 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;u=2;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.high, cmd.kitty_desktop_notification.readOption(.u)); +} + +test "OSC 99: u 4" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;u=bobr;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(.normal, cmd.kitty_desktop_notification.readOption(.u)); +} + +test "OSC 99: w 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;w=0;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(0, cmd.kitty_desktop_notification.readOption(.w)); +} + +test "OSC 99: w 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;w=-1;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w)); +} + +test "OSC 99: w 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;w=-42;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w)); +} + +test "OSC 99: w 4" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "99;w=4294967296;foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_desktop_notification); + try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload); + try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w)); +} diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_dnd_protocol.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_dnd_protocol.zig index 35d23906..839e3961 100644 --- a/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_dnd_protocol.zig +++ b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_dnd_protocol.zig @@ -1,5 +1,12 @@ //! Kitty's drag and drop protocol (OSC 72) -//! Specification: https://sw.kovidgoyal.net/kitty/drag-and-drop-protocol/ +//! +//! This only captures the raw metadata and payload for the OSC. The +//! actual protocol grammar (metadata keys, event types, chunking) is +//! implemented in `terminal/kitty/dnd.zig` and its submodules, since the +//! protocol requires stateful handling that doesn't belong in the +//! stateless OSC parser. +//! +//! Specification: https://sw.kovidgoyal.net/kitty/dnd-protocol/ const std = @import("std"); @@ -9,143 +16,52 @@ const Parser = @import("../../osc.zig").Parser; const Command = @import("../../osc.zig").Command; const Terminator = @import("../../osc.zig").Terminator; -const log = std.log.scoped(.kitty_dnd_protocol); - pub const OSC = struct { - /// The raw metadata that was received. Parse individual values with `readOption`. + /// The raw metadata that was received. Parse with + /// `kitty.dnd.Metadata.parse`. metadata: []const u8, - /// The raw payload. Its meaning and encoding depend on the event type (`t` key). + + /// The raw payload. Its meaning and encoding depend on the event + /// type (`t` metadata key). Null when the OSC had no `;` after the + /// metadata; an empty payload is distinct from no payload. payload: ?[]const u8, + /// The terminator used for this OSC, so any response can match it. terminator: Terminator, - pub fn readOption(self: OSC, comptime key: Option) ?key.Type() { - return key.read(self.metadata); - } -}; + /// We don't currently support encoding this to C in any way. + pub const C = void; -/// Values for the `t` (event type) metadata key. -pub const EventType = enum { - /// ('a') Terminal registers itself as willing to accept drops. - accept_drops, - /// ('A') Terminal unregisters itself; drops should no longer be forwarded. - stop_accepting_drops, - /// ('m') Pointer is moving over the terminal while a drag is in progress. - /// Carries `x`/`y` cursor position; -1 signals the drag left the window. - drop_move, - /// ('M') Items were dropped onto the terminal. - /// Carries `x`/`y` drop position and `i` (multiplexer session ID). - drop_dropped, - /// ('r') Terminal requests data for a specific MIME type from the drag source. - /// Carries `i` (multiplexer session ID) and `y` (1-based MIME type index). - request_data, - /// ('R') Error response to a `request_data` event. - request_error, - /// ('o') Terminal offers data for an outgoing drag (drag-out from terminal). - offer_drag, - /// ('p') Drag source presents the actual payload for a previously requested MIME type. - /// Carries `i` (multiplexer session ID), `o` (operation), and `m` (chunking flag). - present_data, - /// ('P') Replace the current drag image with a new one. - /// Payload is the image data; `X`/`Y` carry image dimensions in pixels. - change_drag_image, - /// ('e') Notification of an event on an outgoing drag offer (e.g., accepted or rejected). - drag_offer_event, - /// ('E') Error on an outgoing drag offer. - drag_offer_error, - /// ('k') URI list data delivered as part of a drag or clipboard transfer. - uri_list_data, - /// ('q') Query terminal capabilities related to the drag-and-drop protocol. - query, - - pub fn init(str: []const u8) ?EventType { - if (str.len != 1) return null; - return switch (str[0]) { - 'a' => .accept_drops, - 'A' => .stop_accepting_drops, - 'm' => .drop_move, - 'M' => .drop_dropped, - 'r' => .request_data, - 'R' => .request_error, - 'o' => .offer_drag, - 'p' => .present_data, - 'P' => .change_drag_image, - 'e' => .drag_offer_event, - 'E' => .drag_offer_error, - 'k' => .uri_list_data, - 'q' => .query, - else => null, - }; + pub fn cval(_: OSC) C { + return {}; } }; -/// Metadata keys defined by the protocol. Keys are case-sensitive: `x` and `X` are distinct. -pub const Option = enum { - /// Event type. Maps to `EventType`; present in every OSC 72 sequence. - t, - /// Chunking flag. `0` = this is the final (or only) chunk; `1` = more chunks follow. - m, - /// Multiplexer session ID. Echoed back in responses so a terminal multiplexer - /// (e.g. tmux) can route data to the correct pane. - i, - /// Drop operation. `0` = reject, `1` = copy, `2` = move, `3` = copy or move. - o, - /// Cursor column in cell units (zero-based). -1 signals the drag has left the window. - x, - /// Cursor row in cell units (zero-based). Also used as a 1-based MIME type index - /// in some events (e.g. `request_data`). -1 signals the drag has left the window. - y, - /// Pixel offset from the left edge of the cell; also used as image width - /// (with `change_drag_image`) or as a symlink/directory marker. - X, - /// Pixel offset from the top edge of the cell; also used as image height - /// (with `change_drag_image`) or as a parent directory handle. - Y, - - pub fn Type(comptime key: Option) type { - return switch (key) { - .t => EventType, - // The spec uses 32-bit signed or unsigned; we standardize on - // i32 because the location keys legitimately take -1 (drag - // leaves the window) and other keys never exceed i32 range. - .m, .i, .o, .x, .y, .X, .Y => i32, - }; - } +pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { + assert(parser.state == .@"72"); - pub fn read(comptime key: Option, metadata: []const u8) ?key.Type() { - const name = @tagName(key); - - const value: []const u8 = value: { - var pos: usize = 0; - while (pos < metadata.len) { - while (pos < metadata.len and std.ascii.isWhitespace(metadata[pos])) pos += 1; - if (pos >= metadata.len) return null; - - // Case-sensitive match: x and X must not be confused. - if (!std.mem.startsWith(u8, metadata[pos..], name)) { - pos = std.mem.indexOfScalarPos(u8, metadata, pos, ':') orelse return null; - pos += 1; - continue; - } - pos += name.len; - - while (pos < metadata.len and std.ascii.isWhitespace(metadata[pos])) pos += 1; - if (pos >= metadata.len) return null; - if (metadata[pos] != '=') return null; - - const end = std.mem.indexOfScalarPos(u8, metadata, pos, ':') orelse metadata.len; - const start = pos + 1; - break :value std.mem.trim(u8, metadata[start..end], &std.ascii.whitespace); - } - return null; - }; - - return switch (key) { - .t => .init(value), - .m, .i, .o, .x, .y, .X, .Y => std.fmt.parseInt(i32, value, 10) catch null, - }; - } -}; + const cap = if (parser.capture) |*c| c else { + parser.state = .invalid; + return null; + }; + + const data = cap.trailing(); + + const metadata: []const u8, const payload: ?[]const u8 = result: { + const sep = std.mem.indexOfScalar(u8, data, ';') orelse break :result .{ data, null }; + break :result .{ data[0..sep], data[sep + 1 .. data.len] }; + }; + + parser.command = .{ + .kitty_dnd_protocol = .{ + .metadata = metadata, + .payload = payload, + .terminator = .init(terminator_ch), + }, + }; + + return &parser.command; +} test "OSC 72: metadata only, no payload" { const testing = std.testing; @@ -192,134 +108,19 @@ test "OSC 72: metadata and non-empty payload" { try testing.expectEqualStrings("text/plain text/uri-list", cmd.kitty_dnd_protocol.payload.?); } -test "OSC 72: readOption .t valid event types" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const cases = .{ - .{ "72;t=a", EventType.accept_drops }, - .{ "72;t=A", EventType.stop_accepting_drops }, - .{ "72;t=m", EventType.drop_move }, - .{ "72;t=M", EventType.drop_dropped }, - .{ "72;t=r", EventType.request_data }, - .{ "72;t=R", EventType.request_error }, - .{ "72;t=o", EventType.offer_drag }, - .{ "72;t=p", EventType.present_data }, - .{ "72;t=P", EventType.change_drag_image }, - .{ "72;t=e", EventType.drag_offer_event }, - .{ "72;t=E", EventType.drag_offer_error }, - .{ "72;t=k", EventType.uri_list_data }, - .{ "72;t=q", EventType.query }, - }; - - inline for (cases) |case| { - p.deinit(); - p = .init(testing.allocator); - for (case[0]) |ch| p.next(ch); - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_dnd_protocol); - try testing.expectEqual(case[1], cmd.kitty_dnd_protocol.readOption(.t).?); - } -} - -test "OSC 72: readOption .t unknown value returns null" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "72;t=z"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_dnd_protocol); - try testing.expect(cmd.kitty_dnd_protocol.readOption(.t) == null); -} - -test "OSC 72: readOption integer keys" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "72;t=m:i=3:x=10:y=5:X=320:Y=200:o=1:m=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_dnd_protocol); - try testing.expectEqual(@as(i32, 3), cmd.kitty_dnd_protocol.readOption(.i).?); - try testing.expectEqual(@as(i32, 10), cmd.kitty_dnd_protocol.readOption(.x).?); - try testing.expectEqual(@as(i32, 5), cmd.kitty_dnd_protocol.readOption(.y).?); - try testing.expectEqual(@as(i32, 320), cmd.kitty_dnd_protocol.readOption(.X).?); - try testing.expectEqual(@as(i32, 200), cmd.kitty_dnd_protocol.readOption(.Y).?); - try testing.expectEqual(@as(i32, 1), cmd.kitty_dnd_protocol.readOption(.o).?); - try testing.expectEqual(@as(i32, 0), cmd.kitty_dnd_protocol.readOption(.m).?); -} - -test "OSC 72: readOption negative sentinel (-1 for drag leave)" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "72;t=m:x=-1:y=-1"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_dnd_protocol); - try testing.expectEqual(@as(i32, -1), cmd.kitty_dnd_protocol.readOption(.x).?); - try testing.expectEqual(@as(i32, -1), cmd.kitty_dnd_protocol.readOption(.y).?); -} - -test "OSC 72: readOption case-sensitive key matching" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - // x=10 must not be returned when asking for .X - const input = "72;x=10:Y=200"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_dnd_protocol); - try testing.expectEqual(@as(i32, 10), cmd.kitty_dnd_protocol.readOption(.x).?); - try testing.expect(cmd.kitty_dnd_protocol.readOption(.X) == null); - try testing.expectEqual(@as(i32, 200), cmd.kitty_dnd_protocol.readOption(.Y).?); - try testing.expect(cmd.kitty_dnd_protocol.readOption(.y) == null); -} - -test "OSC 72: readOption absent key returns null" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "72;t=a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_dnd_protocol); - try testing.expect(cmd.kitty_dnd_protocol.readOption(.i) == null); - try testing.expect(cmd.kitty_dnd_protocol.readOption(.x) == null); - try testing.expect(cmd.kitty_dnd_protocol.readOption(.X) == null); - try testing.expect(cmd.kitty_dnd_protocol.readOption(.m) == null); -} - -test "OSC 72: readOption malformed integer returns null" { +test "OSC 72: empty metadata with payload" { const testing = std.testing; var p: Parser = .init(testing.allocator); defer p.deinit(); - const input = "72;x=notanumber"; + const input = "72;;payload"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_dnd_protocol); - try testing.expect(cmd.kitty_dnd_protocol.readOption(.x) == null); + try testing.expectEqualStrings("", cmd.kitty_dnd_protocol.metadata); + try testing.expectEqualStrings("payload", cmd.kitty_dnd_protocol.payload.?); } test "OSC 72: BEL terminator recorded" { @@ -335,29 +136,3 @@ test "OSC 72: BEL terminator recorded" { try testing.expect(cmd == .kitty_dnd_protocol); try testing.expect(cmd.kitty_dnd_protocol.terminator == .bel); } - -pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { - assert(parser.state == .@"72"); - - const cap = if (parser.capture) |*c| c else { - parser.state = .invalid; - return null; - }; - - const data = cap.trailing(); - - const metadata: []const u8, const payload: ?[]const u8 = result: { - const sep = std.mem.indexOfScalar(u8, data, ';') orelse break :result .{ data, null }; - break :result .{ data[0..sep], data[sep + 1 .. data.len] }; - }; - - parser.command = .{ - .kitty_dnd_protocol = .{ - .metadata = metadata, - .payload = payload, - .terminator = .init(terminator_ch), - }, - }; - - return &parser.command; -} diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_text_sizing.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_text_sizing.zig index 94f27100..a60ed5d7 100644 --- a/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_text_sizing.zig +++ b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_text_sizing.zig @@ -77,7 +77,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { }; // Write a NUL byte to ensure that `text` is NUL-terminated - cap.writer.writeByte(0) catch { + cap.writeByte(0) catch { parser.state = .invalid; return null; }; diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig index 97212fbb..e060f8bf 100644 --- a/vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig +++ b/vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig @@ -1,6 +1,7 @@ //! https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md const std = @import("std"); +const lib = @import("../../lib.zig"); const Parser = @import("../../osc.zig").Parser; const OSCCommand = @import("../../osc.zig").Command; const string_encoding = @import("../../../os/string_encoding.zig"); @@ -68,7 +69,10 @@ pub const Command = struct { // See https://github.com/ghostty-org/ghostty/issues/10865 and // https://github.com/kovidgoyal/kitty/issues/9500 // for further details. -pub const ClickEvents = enum { absolute, relative }; +pub const ClickEvents = lib.Enum( + lib.target, + &.{ "absolute", "relative" }, +); pub const Option = enum { aid, @@ -199,7 +203,7 @@ pub const Option = enum { return switch (self) { .aid => value, - .cl => .init(value), + .cl => parseClick(value), .prompt_kind => if (value.len == 1) PromptKind.init(value[0]) else null, .err => value, .redraw => if (std.mem.eql(u8, value, "0")) @@ -234,42 +238,50 @@ pub const Option = enum { /// The `cl` option specifies what kind of cursor key sequences are handled /// by the application for click-to-move-cursor functionality. -pub const Click = enum { - /// Value: "line". Allows motion within a single input line using standard - /// left/right arrow escape sequences. Only a single left/right sequence - /// should be emitted for double-width characters. - line, - - /// Value: "m". Allows movement between different lines in the same group, - /// but only using left/right arrow escape sequences. - multiple, - - /// Value: "v". Like `multiple` but cursor up/down should be used. The - /// terminal should be conservative when moving between lines: move the - /// cursor left to the start of line, emit the needed up/down sequences, - /// then move the cursor right to the clicked destination. - conservative_vertical, - - /// Value: "w". Like `conservative_vertical` but specifies that there are - /// no spurious spaces at the end of the line, and the application editor - /// handles "smart vertical movement" (moving 2 lines up from position 20, - /// where the intermediate line is 15 chars wide and the destination is - /// 18 chars wide, ends at position 18). - smart_vertical, - - pub fn init(value: []const u8) ?Click { - return if (value.len == 1) switch (value[0]) { - 'm' => .multiple, - 'v' => .conservative_vertical, - 'w' => .smart_vertical, - else => null, - } else if (std.mem.eql( - u8, - value, - "line", - )) .line else null; - } -}; +/// +/// `line` allows movement within one input line. `multiple` allows movement +/// across lines with left/right sequences. The two vertical modes additionally +/// allow up/down sequences, with `smart_vertical` permitting editor-aware +/// column clamping. +pub const Click = lib.Enum( + lib.target, + &.{ + // Value: "line". Allows motion within a single input line using + // standard left/right arrow escape sequences. Only a single left/right + // sequence should be emitted for double-width characters. + "line", + + // Value: "m". Allows movement between different lines in the same + // group, but only using left/right arrow escape sequences. + "multiple", + + // Value: "v". Like `multiple` but cursor up/down should be used. The + // terminal should be conservative when moving between lines: move the + // cursor left to the start of line, emit the needed up/down sequences, + // then move the cursor right to the clicked destination. + "conservative_vertical", + + // Value: "w". Like `conservative_vertical` but specifies that there + // are no spurious spaces at the end of the line, and the application + // editor handles "smart vertical movement" (moving 2 lines up from + // position 20, where the intermediate line is 15 chars wide and the + // destination is 18 chars wide, ends at position 18). + "smart_vertical", + }, +); + +fn parseClick(value: []const u8) ?Click { + return if (value.len == 1) switch (value[0]) { + 'm' => .multiple, + 'v' => .conservative_vertical, + 'w' => .smart_vertical, + else => null, + } else if (std.mem.eql( + u8, + value, + "line", + )) .line else null; +} pub const PromptKind = enum { initial, |
