From 07f33ad91453ebf9bbfdc24a26d935bcb949d08a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 16 Feb 2026 20:27:54 -0600 Subject: [PATCH] osc 99 & 5522: share metadata parsing code Reduce redundant code by sharing the metadata parsing code between the OSC 99 & OSC 5522 parsers. --- src/terminal/osc/lib.zig | 61 +++++++++++++++++ .../osc/parsers/kitty_clipboard_protocol.zig | 38 ++--------- .../parsers/kitty_desktop_notification.zig | 65 ++----------------- 3 files changed, 72 insertions(+), 92 deletions(-) create mode 100644 src/terminal/osc/lib.zig diff --git a/src/terminal/osc/lib.zig b/src/terminal/osc/lib.zig new file mode 100644 index 000000000..dfa1a59b7 --- /dev/null +++ b/src/terminal/osc/lib.zig @@ -0,0 +1,61 @@ +const std = @import("std"); + +/// Used to iterate over matching key/values in the metadata +pub fn Iterator(comptime Option: type, comptime isValidMetadataValue: fn ([]const u8) bool, comptime key: Option) type { + return struct { + const Self = @This(); + + metadata: []const u8, + pos: usize, + + pub fn init(metadata: []const u8) Self { + return .{ + .metadata = metadata, + .pos = 0, + }; + } + + /// Return the value of the next matching key. The value is guaranteed + /// to be `null` or a valid metadata value. + pub fn next(self: *Self) ?[]const u8 { + // bail if we are out of metadata + if (self.pos >= self.metadata.len) return null; + while (self.pos < self.metadata.len) { + // skip any whitespace + while (self.pos < self.metadata.len and std.ascii.isWhitespace(self.metadata[self.pos])) self.pos += 1; + // bail if we are out of metadata + if (self.pos >= self.metadata.len) return null; + if (!std.mem.startsWith(u8, self.metadata[self.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 + self.pos = std.mem.indexOfScalarPos(u8, self.metadata, self.pos, ':') orelse { + self.pos = self.metadata.len; + return null; + }; + self.pos += 1; + continue; + } + // skip past the key + self.pos += @tagName(key).len; + // skip any whitespace + while (self.pos < self.metadata.len and std.ascii.isWhitespace(self.metadata[self.pos])) self.pos += 1; + // bail if we are out of metadata + if (self.pos >= self.metadata.len) return null; + // a valid option has an '=' + if (self.metadata[self.pos] != '=') return null; + // the end of the value is bounded by a ':' or the end of the metadata + const end = std.mem.indexOfScalarPos(u8, self.metadata, self.pos, ':') orelse self.metadata.len; + const start = self.pos + 1; + self.pos = end + 1; + // strip any leading or trailing whitespace + const value = std.mem.trim(u8, self.metadata[start..end], &std.ascii.whitespace); + // if this is not a valid value, skip it + if (!@call(.always_inline, isValidMetadataValue, .{value})) continue; + // return the value + return value; + } + // the key was not found + return null; + } + }; +} diff --git a/src/terminal/osc/parsers/kitty_clipboard_protocol.zig b/src/terminal/osc/parsers/kitty_clipboard_protocol.zig index c257edd10..db6efc048 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 Iterator = @import("../lib.zig").Iterator; 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: Iterator(Option, isValidMetadataValue, key) = .init(metadata); + const value = it.next() orelse return null; // return the parsed value return switch (key) { @@ -155,6 +127,10 @@ fn parseIdentifier(str: []const u8) ?[]const u8 { return null; } +fn isValidMetadataValue(_: []const u8) bool { + return true; +} + pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { assert(parser.state == .@"5522"); diff --git a/src/terminal/osc/parsers/kitty_desktop_notification.zig b/src/terminal/osc/parsers/kitty_desktop_notification.zig index edde49cec..9d22c86af 100644 --- a/src/terminal/osc/parsers/kitty_desktop_notification.zig +++ b/src/terminal/osc/parsers/kitty_desktop_notification.zig @@ -9,6 +9,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 Iterator = @import("../lib.zig").Iterator; const encoding = @import("../encoding.zig"); const lib = @import("../../../lib/main.zig"); const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; @@ -136,11 +137,11 @@ pub const Option = enum { .f => ?[]const u8, .g => ?[]const u8, .i => ?[]const u8, - .n => Iterator(.n), + .n => Iterator(Option, isValidMetadataValue, .n), .o => Occasion, .p => Payload, .s => []const u8, - .t => Iterator(.t), + .t => Iterator(Option, isValidMetadataValue, .t), .u => Urgency, .w => i32, }; @@ -173,7 +174,7 @@ pub const Option = enum { comptime key: Option, metadata: []const u8, ) key.Type() { - var it: Iterator(key) = switch (key) { + var it: Iterator(Option, isValidMetadataValue, key) = switch (key) { inline .t, .n => return .init(metadata), inline else => .init(metadata), }; @@ -279,64 +280,6 @@ fn parseIdentifier(str: []const u8) ?[]const u8 { return null; } -/// Used to iterate over matching key/values in the metadata -pub fn Iterator(comptime key: Option) type { - return struct { - metadata: []const u8, - pos: usize, - - pub fn init(metadata: []const u8) Iterator(key) { - return .{ - .metadata = metadata, - .pos = 0, - }; - } - - /// Return the value of the next matching key. The value is guaranteed - /// to be `null` or a valid metadata value. - pub fn next(self: *Iterator(key)) ?[]const u8 { - // bail if we are out of metadata - if (self.pos >= self.metadata.len) return null; - while (self.pos < self.metadata.len) { - // skip any whitespace - while (self.pos < self.metadata.len and std.ascii.isWhitespace(self.metadata[self.pos])) self.pos += 1; - // bail if we are out of metadata - if (self.pos >= self.metadata.len) return null; - if (self.metadata[self.pos] != @tagName(key)[0]) { - // this isn't the key we are looking for, skip to the next option, or bail if - // there is no next option - self.pos = std.mem.indexOfScalarPos(u8, self.metadata, self.pos, ':') orelse { - self.pos = self.metadata.len; - return null; - }; - self.pos += 1; - continue; - } - // skip past the key - self.pos += 1; - // skip any whitespace - while (self.pos < self.metadata.len and std.ascii.isWhitespace(self.metadata[self.pos])) self.pos += 1; - // bail if we are out of metadata - if (self.pos >= self.metadata.len) return null; - // a valid option has an '=' - if (self.metadata[self.pos] != '=') return null; - // the end of the value is bounded by a ':' or the end of the metadata - const end = std.mem.indexOfScalarPos(u8, self.metadata, self.pos, ':') orelse self.metadata.len; - const start = self.pos + 1; - self.pos = end + 1; - // strip any leading or trailing whitespace - const value = std.mem.trim(u8, self.metadata[start..end], &std.ascii.whitespace); - // if this is not a valid value, skip it - if (!isValidMetadataValue(value)) continue; - // return the value - return value; - } - // the key was not found - return null; - } - }; -} - pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { assert(parser.state == .@"99");