Merge branch 'ghostty-org:main' into main

This commit is contained in:
Mohammad AlShami
2026-08-22 01:28:42 +03:00
committed by Mohammad H. AlShami
8 changed files with 1327 additions and 33 deletions

View File

@@ -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;

View File

@@ -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"));

View File

@@ -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| {

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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) {

File diff suppressed because it is too large Load Diff

View File

@@ -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});