aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/osc
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/osc
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/osc')
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/kitty_metadata.zig94
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers.zig1
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig20
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/color.zig25
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/kitty_clipboard_protocol.zig41
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/kitty_desktop_notification.zig1201
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/kitty_dnd_protocol.zig319
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/kitty_text_sizing.zig2
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig88
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,