osc 99 & 5522: share metadata parsing code

Reduce redundant code by sharing the metadata parsing code
between the OSC 99 & OSC 5522 parsers.
This commit is contained in:
Jeffrey C. Ollie
2026-02-16 20:27:54 -06:00
committed by Mitchell Hashimoto
parent aa4ec3508e
commit 07f33ad914
3 changed files with 72 additions and 92 deletions

61
src/terminal/osc/lib.zig Normal file
View File

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

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

View File

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