diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h index f7626383c..7a681c3b4 100644 --- a/include/ghostty/vt/osc.h +++ b/include/ghostty/vt/osc.h @@ -66,6 +66,7 @@ typedef enum GHOSTTY_ENUM_TYPED { GHOSTTY_OSC_COMMAND_KITTY_CLIPBOARD_PROTOCOL = 23, GHOSTTY_OSC_COMMAND_KITTY_DND_PROTOCOL = 24, GHOSTTY_OSC_COMMAND_CONTEXT_SIGNAL = 25, + GHOSTTY_OSC_COMMAND_KITTY_DESKTOP_NOTIFICATION = 26, GHOSTTY_OSC_COMMAND_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyOscCommandType; diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index bc214c576..95a166c3d 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -1000,6 +1000,7 @@ test "manifest uses public enum names" { try std.testing.expectEqual(@as(i64, 23), osc_values.get("KITTY_CLIPBOARD_PROTOCOL").?.integer); try std.testing.expectEqual(@as(i64, 24), osc_values.get("KITTY_DND_PROTOCOL").?.integer); try std.testing.expectEqual(@as(i64, 25), osc_values.get("CONTEXT_SIGNAL").?.integer); + try std.testing.expectEqual(@as(i64, 26), osc_values.get("KITTY_DESKTOP_NOTIFICATION").?.integer); try std.testing.expect(osc_values.contains("TYPE_MAX_VALUE")); try std.testing.expect(!osc_values.contains("MAX_VALUE")); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 618777241..7edabcdc0 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -163,12 +163,17 @@ pub const Command = union(Key) { /// https://uapi-group.org/specifications/specs/osc_context/ context_signal: parsers.context_signal.Command, + /// Kitty desktop notifications (OSC 99) + kitty_desktop_notification: KittyDesktopNotification, + pub const SemanticPrompt = parsers.semantic_prompt.Command; pub const KittyClipboardProtocol = parsers.kitty_clipboard_protocol.OSC; pub const KittyDndProtocol = parsers.kitty_dnd_protocol.OSC; + pub const KittyDesktopNotification = parsers.kitty_desktop_notification.OSC; + pub const Key = LibEnum( lib.target, // NOTE: Order matters, see LibEnum documentation. @@ -199,6 +204,7 @@ pub const Command = union(Key) { "kitty_clipboard_protocol", "kitty_dnd_protocol", "context_signal", + "kitty_desktop_notification", }, ); @@ -364,6 +370,7 @@ pub const Parser = struct { @"66", @"72", @"77", + @"99", @"104", @"110", @"111", @@ -444,6 +451,7 @@ pub const Parser = struct { .kitty_text_sizing, .kitty_clipboard_protocol, .kitty_dnd_protocol, + .kitty_desktop_notification, .context_signal, => {}, } @@ -783,11 +791,24 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"9", + => switch (c) { + ';' => self.captureTrailing(.fixed), + '9' => self.state = .@"99", + else => self.state = .invalid, + }, + + .@"99", + => switch (c) { + // OSC 99 encoded payloads can exceed the fixed buffer. + ';' => self.captureTrailing(.allocating), + else => self.state = .invalid, + }, + .@"0", .@"22", .@"777", .@"8", - .@"9", => switch (c) { ';' => self.captureTrailing(.fixed), else => self.state = .invalid, @@ -868,6 +889,8 @@ pub const Parser = struct { .@"77" => null, + .@"99" => parsers.kitty_desktop_notification.parse(self, terminator_ch), + .@"133" => parsers.semantic_prompt.parse(self, terminator_ch), .@"552" => null, @@ -888,7 +911,7 @@ test { test "Parser allocating captures have a hard limit" { const testing = std.testing; - const prefixes = [_][]const u8{ "52;", "66;", "72;", "5522;" }; + const prefixes = [_][]const u8{ "52;", "66;", "72;", "99;", "5522;" }; const limit = Parser.MAX_BUF + 1; for (prefixes) |prefix| { diff --git a/src/terminal/osc/kitty_metadata.zig b/src/terminal/osc/kitty_metadata.zig new file mode 100644 index 000000000..469c97a0f --- /dev/null +++ b/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/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig index 8cb91fb94..148bfa335 100644 --- a/src/terminal/osc/parsers.zig +++ b/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/src/terminal/osc/parsers/kitty_clipboard_protocol.zig b/src/terminal/osc/parsers/kitty_clipboard_protocol.zig index c257edd10..3b32aae8b 100644 --- a/src/terminal/osc/parsers/kitty_clipboard_protocol.zig +++ b/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); @@ -96,37 +97,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/src/terminal/osc/parsers/kitty_desktop_notification.zig b/src/terminal/osc/parsers/kitty_desktop_notification.zig new file mode 100644 index 000000000..27fc86702 --- /dev/null +++ b/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/src/terminal/stream.zig b/src/terminal/stream.zig index 4308adf9f..cb684fa50 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -2570,6 +2570,7 @@ pub fn Stream(comptime H: type) type { .conemu_run_process, .kitty_text_sizing, .kitty_dnd_protocol, + .kitty_desktop_notification, .context_signal, => { log.debug("unimplemented OSC callback: {}", .{cmd});