aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/osc
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/osc
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/osc')
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/encoding.zig38
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers.zig21
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/change_window_icon.zig33
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/change_window_title.zig119
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig106
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/color.zig780
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/context_signal.zig541
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/hyperlink.zig164
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/iterm2.zig435
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/kitty_clipboard_protocol.zig702
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/kitty_color.zig212
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/kitty_text_sizing.zig255
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/mouse_shape.zig39
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/osc9.zig1141
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/report_pwd.zig48
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/rxvt_extension.zig59
-rw-r--r--vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig1264
17 files changed, 5957 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/osc/encoding.zig b/vendor/libghostty-vt/src/terminal/osc/encoding.zig
new file mode 100644
index 00000000..7491d10c
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/encoding.zig
@@ -0,0 +1,38 @@
+//! Specialized encodings used in some OSC protocols.
+const std = @import("std");
+
+/// Kitty defines "Escape code safe UTF-8" as valid UTF-8 with the
+/// additional requirement of not containing any C0 escape codes
+/// (0x00-0x1f), DEL (0x7f) and C1 escape codes (0x80-0x9f).
+///
+/// Used by OSC 66 (text sizing) and OSC 99 (Kitty notifications).
+///
+/// See: https://sw.kovidgoyal.net/kitty/desktop-notifications/#safe-utf8
+pub fn isSafeUtf8(s: []const u8) bool {
+ const utf8 = std.unicode.Utf8View.init(s) catch {
+ @branchHint(.cold);
+ return false;
+ };
+
+ var it = utf8.iterator();
+ while (it.nextCodepoint()) |cp| switch (cp) {
+ 0x00...0x1f, 0x7f, 0x80...0x9f => {
+ @branchHint(.cold);
+ return false;
+ },
+ else => {},
+ };
+
+ return true;
+}
+
+test isSafeUtf8 {
+ const testing = std.testing;
+
+ try testing.expect(isSafeUtf8("Hello world!"));
+ try testing.expect(isSafeUtf8("安全的ユニコード☀️"));
+ try testing.expect(!isSafeUtf8("No linebreaks\nallowed"));
+ try testing.expect(!isSafeUtf8("\x07no bells"));
+ try testing.expect(!isSafeUtf8("\x1b]9;no OSCs\x1b\\\x1b[m"));
+ try testing.expect(!isSafeUtf8("\x9f8-bit escapes are clever, but no"));
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers.zig b/vendor/libghostty-vt/src/terminal/osc/parsers.zig
new file mode 100644
index 00000000..a56b38e8
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers.zig
@@ -0,0 +1,21 @@
+const std = @import("std");
+
+pub const change_window_icon = @import("parsers/change_window_icon.zig");
+pub const context_signal = @import("parsers/context_signal.zig");
+pub const change_window_title = @import("parsers/change_window_title.zig");
+pub const clipboard_operation = @import("parsers/clipboard_operation.zig");
+pub const color = @import("parsers/color.zig");
+pub const hyperlink = @import("parsers/hyperlink.zig");
+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_text_sizing = @import("parsers/kitty_text_sizing.zig");
+pub const mouse_shape = @import("parsers/mouse_shape.zig");
+pub const osc9 = @import("parsers/osc9.zig");
+pub const report_pwd = @import("parsers/report_pwd.zig");
+pub const rxvt_extension = @import("parsers/rxvt_extension.zig");
+pub const semantic_prompt = @import("parsers/semantic_prompt.zig");
+
+test {
+ std.testing.refAllDecls(@This());
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/change_window_icon.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/change_window_icon.zig
new file mode 100644
index 00000000..eeae6a71
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/change_window_icon.zig
@@ -0,0 +1,33 @@
+const std = @import("std");
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+/// Parse OSC 1
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ cap.writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ parser.command = .{
+ .change_window_icon = data[0 .. data.len - 1 :0],
+ };
+ return &parser.command;
+}
+
+test "OSC 1: change_window_icon" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ p.next('1');
+ p.next(';');
+ p.next('a');
+ p.next('b');
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .change_window_icon);
+ try testing.expectEqualStrings("ab", cmd.change_window_icon);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/change_window_title.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/change_window_title.zig
new file mode 100644
index 00000000..5bd84dc3
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/change_window_title.zig
@@ -0,0 +1,119 @@
+const std = @import("std");
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+/// Parse OSC 0 and OSC 2
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ cap.writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ parser.command = .{
+ .change_window_title = data[0 .. data.len - 1 :0],
+ };
+ return &parser.command;
+}
+
+test "OSC 0: change_window_title" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ p.next('0');
+ p.next(';');
+ p.next('a');
+ p.next('b');
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .change_window_title);
+ try testing.expectEqualStrings("ab", cmd.change_window_title);
+}
+
+test "OSC 0: longer than buffer" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2);
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 0: one shorter than buffer length" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const prefix = "0;";
+ const title = "a" ** (Parser.MAX_BUF - 1);
+ const input = prefix ++ title;
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .change_window_title);
+ try testing.expectEqualStrings(title, cmd.change_window_title);
+}
+
+test "OSC 0: exactly at buffer length" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const prefix = "0;";
+ const title = "a" ** Parser.MAX_BUF;
+ const input = prefix ++ title;
+ for (input) |ch| p.next(ch);
+
+ // This should be null because we always reserve space for a null terminator.
+ try testing.expect(p.end(null) == null);
+}
+test "OSC 2: change_window_title with 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ p.next('2');
+ p.next(';');
+ p.next('a');
+ p.next('b');
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .change_window_title);
+ try testing.expectEqualStrings("ab", cmd.change_window_title);
+}
+
+test "OSC 2: change_window_title with utf8" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ p.next('2');
+ p.next(';');
+ // '—' EM DASH U+2014 (E2 80 94)
+ p.next(0xE2);
+ p.next(0x80);
+ p.next(0x94);
+
+ p.next(' ');
+ // '‐' HYPHEN U+2010 (E2 80 90)
+ // Intententionally chosen to conflict with the 0x90 C1 control
+ p.next(0xE2);
+ p.next(0x80);
+ p.next(0x90);
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .change_window_title);
+ try testing.expectEqualStrings("— ‐", cmd.change_window_title);
+}
+
+test "OSC 2: change_window_title empty" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ p.next('2');
+ p.next(';');
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .change_window_title);
+ try testing.expectEqualStrings("", cmd.change_window_title);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig
new file mode 100644
index 00000000..8d149f97
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/clipboard_operation.zig
@@ -0,0 +1,106 @@
+const std = @import("std");
+
+const assert = @import("../../../quirks.zig").inlineAssert;
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+/// Parse OSC 52
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ assert(parser.state == .@"52");
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ cap.writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ if (data.len == 1) {
+ parser.state = .invalid;
+ return null;
+ }
+ if (data[0] == ';') {
+ parser.command = .{
+ .clipboard_contents = .{
+ .kind = 'c',
+ .data = data[1 .. data.len - 1 :0],
+ },
+ };
+ } else {
+ if (data.len < 2) {
+ parser.state = .invalid;
+ return null;
+ }
+ if (data[1] != ';') {
+ parser.state = .invalid;
+ return null;
+ }
+ parser.command = .{
+ .clipboard_contents = .{
+ .kind = data[0],
+ .data = data[2 .. data.len - 1 :0],
+ },
+ };
+ }
+ return &parser.command;
+}
+
+test "OSC 52: get/set clipboard" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "52;s;?";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .clipboard_contents);
+ try testing.expect(cmd.clipboard_contents.kind == 's');
+ try testing.expectEqualStrings("?", cmd.clipboard_contents.data);
+}
+
+test "OSC 52: get/set clipboard (optional parameter)" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "52;;?";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .clipboard_contents);
+ try testing.expect(cmd.clipboard_contents.kind == 'c');
+ try testing.expectEqualStrings("?", cmd.clipboard_contents.data);
+}
+
+test "OSC 52: get/set clipboard with allocator" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "52;s;?";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .clipboard_contents);
+ try testing.expect(cmd.clipboard_contents.kind == 's');
+ try testing.expectEqualStrings("?", cmd.clipboard_contents.data);
+}
+
+test "OSC 52: clear clipboard" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ defer p.deinit();
+
+ const input = "52;;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .clipboard_contents);
+ try testing.expect(cmd.clipboard_contents.kind == 'c');
+ try testing.expectEqualStrings("", cmd.clipboard_contents.data);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/color.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/color.zig
new file mode 100644
index 00000000..547c98ea
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/color.zig
@@ -0,0 +1,780 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const DynamicColor = @import("../../color.zig").Dynamic;
+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 log = std.log.scoped(.osc_color);
+
+const ParseError = Allocator.Error || error{
+ MissingOperation,
+};
+
+/// The possible operations we support for colors.
+pub const Operation = enum {
+ osc_4,
+ osc_5,
+ osc_10,
+ osc_11,
+ osc_12,
+ osc_13,
+ osc_14,
+ osc_15,
+ osc_16,
+ osc_17,
+ osc_18,
+ osc_19,
+ osc_104,
+ osc_105,
+ osc_110,
+ osc_111,
+ osc_112,
+ osc_113,
+ osc_114,
+ osc_115,
+ osc_116,
+ osc_117,
+ osc_118,
+ osc_119,
+};
+
+/// Parse OSCs 4, 5, 10-19, 104, 110-119
+pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command {
+ const alloc = parser.alloc orelse {
+ parser.state = .invalid;
+ return null;
+ };
+ // If we've collected any extra data parse that, otherwise use an empty
+ // string.
+ const data = data: {
+ const cap = if (parser.capture) |*c| c else break :data "";
+ break :data cap.trailing();
+ };
+ // Check and make sure that we're parsing the correct OSCs
+ const op: Operation = switch (parser.state) {
+ .@"4" => .osc_4,
+ .@"5" => .osc_5,
+ .@"10" => .osc_10,
+ .@"11" => .osc_11,
+ .@"12" => .osc_12,
+ .@"13" => .osc_13,
+ .@"14" => .osc_14,
+ .@"15" => .osc_15,
+ .@"16" => .osc_16,
+ .@"17" => .osc_17,
+ .@"18" => .osc_18,
+ .@"19" => .osc_19,
+ .@"104" => .osc_104,
+ .@"110" => .osc_110,
+ .@"111" => .osc_111,
+ .@"112" => .osc_112,
+ .@"113" => .osc_113,
+ .@"114" => .osc_114,
+ .@"115" => .osc_115,
+ .@"116" => .osc_116,
+ .@"117" => .osc_117,
+ .@"118" => .osc_118,
+ .@"119" => .osc_119,
+ else => {
+ parser.state = .invalid;
+ return null;
+ },
+ };
+ parser.command = .{
+ .color_operation = .{
+ .op = op,
+ .requests = parseColor(alloc, op, data) catch |err| list: {
+ log.info(
+ "failed to parse OSC {t} color request err={} data={s}",
+ .{ parser.state, err, data },
+ );
+ break :list .{};
+ },
+ .terminator = .init(terminator_ch),
+ },
+ };
+ return &parser.command;
+}
+
+test "OSC 4: empty param" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "4;;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b');
+ try testing.expect(cmd == null);
+}
+
+/// Parse any color operation string. This should NOT include the operation
+/// itself, but only the body of the operation. e.g. for "4;a;b;c" the body
+/// should be "a;b;c" and the operation should be set accordingly.
+///
+/// Color parsing is fairly complicated so we pull this out to a specialized
+/// function rather than go through our OSC parsing state machine. This is
+/// much slower and requires more memory (since we need to buffer the full
+/// request) but grants us an easier to understand and testable implementation.
+///
+/// If color changing ends up being a bottleneck we can optimize this later.
+fn parseColor(
+ alloc: Allocator,
+ op: Operation,
+ buf: []const u8,
+) ParseError!List {
+ var it = std.mem.tokenizeScalar(u8, buf, ';');
+ return switch (op) {
+ .osc_4 => try parseGetSetAnsiColor(alloc, .osc_4, &it),
+ .osc_5 => try parseGetSetAnsiColor(alloc, .osc_5, &it),
+ .osc_104 => try parseResetAnsiColor(alloc, .osc_104, &it),
+ .osc_105 => try parseResetAnsiColor(alloc, .osc_105, &it),
+ .osc_10 => try parseGetSetDynamicColor(alloc, .foreground, &it),
+ .osc_11 => try parseGetSetDynamicColor(alloc, .background, &it),
+ .osc_12 => try parseGetSetDynamicColor(alloc, .cursor, &it),
+ .osc_13 => try parseGetSetDynamicColor(alloc, .pointer_foreground, &it),
+ .osc_14 => try parseGetSetDynamicColor(alloc, .pointer_background, &it),
+ .osc_15 => try parseGetSetDynamicColor(alloc, .tektronix_foreground, &it),
+ .osc_16 => try parseGetSetDynamicColor(alloc, .tektronix_background, &it),
+ .osc_17 => try parseGetSetDynamicColor(alloc, .highlight_background, &it),
+ .osc_18 => try parseGetSetDynamicColor(alloc, .tektronix_cursor, &it),
+ .osc_19 => try parseGetSetDynamicColor(alloc, .highlight_foreground, &it),
+ .osc_110 => try parseResetDynamicColor(alloc, .foreground, &it),
+ .osc_111 => try parseResetDynamicColor(alloc, .background, &it),
+ .osc_112 => try parseResetDynamicColor(alloc, .cursor, &it),
+ .osc_113 => try parseResetDynamicColor(alloc, .pointer_foreground, &it),
+ .osc_114 => try parseResetDynamicColor(alloc, .pointer_background, &it),
+ .osc_115 => try parseResetDynamicColor(alloc, .tektronix_foreground, &it),
+ .osc_116 => try parseResetDynamicColor(alloc, .tektronix_background, &it),
+ .osc_117 => try parseResetDynamicColor(alloc, .highlight_background, &it),
+ .osc_118 => try parseResetDynamicColor(alloc, .tektronix_cursor, &it),
+ .osc_119 => try parseResetDynamicColor(alloc, .highlight_foreground, &it),
+ };
+}
+
+/// OSC 4/5
+fn parseGetSetAnsiColor(
+ alloc: Allocator,
+ comptime op: Operation,
+ it: *std.mem.TokenIterator(u8, .scalar),
+) Allocator.Error!List {
+ // Note: in ANY error scenario below we return the accumulated results.
+ // This matches the xterm behavior (see misc.c ChangeAnsiColorRequest)
+
+ var result: List = .{};
+ errdefer result.deinit(alloc);
+ while (true) {
+ // We expect a `c; spec` pair. If either doesn't exist then
+ // we return the results up to this point.
+ const color_str = it.next() orelse return result;
+ const spec_str = it.next() orelse return result;
+
+ // Color must be numeric. u9 because that'll fit our palette + special
+ const color: u9 = std.fmt.parseInt(
+ u9,
+ color_str,
+ 10,
+ ) catch return result;
+
+ // Parse the color.
+ const target: Target = switch (op) {
+ // OSC5 maps directly to the Special enum.
+ .osc_5 => .{ .special = std.meta.intToEnum(
+ SpecialColor,
+ std.math.cast(u3, color) orelse return result,
+ ) catch 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(
+ SpecialColor,
+ std.math.cast(u3, color - 256) orelse return result,
+ ) catch return result },
+
+ else => comptime unreachable,
+ };
+
+ // "?" always results in a query.
+ if (std.mem.eql(u8, spec_str, "?")) {
+ const req = try result.addOne(alloc);
+ req.* = .{ .query = target };
+ continue;
+ }
+
+ const rgb = RGB.parse(spec_str) catch return result;
+ const req = try result.addOne(alloc);
+ req.* = .{ .set = .{
+ .target = target,
+ .color = rgb,
+ } };
+ }
+}
+
+/// OSC 104/105: Reset ANSI Colors
+fn parseResetAnsiColor(
+ alloc: Allocator,
+ comptime op: Operation,
+ it: *std.mem.TokenIterator(u8, .scalar),
+) Allocator.Error!List {
+ // Note: xterm stops parsing the reset list on any error, but we're
+ // more flexible and try the next value. This matches the behavior of
+ // Kitty and I don't see a downside to being more flexible here. Hopefully
+ // no one depends on the exact behavior of xterm.
+
+ var result: List = .{};
+ errdefer result.deinit(alloc);
+ while (true) {
+ const color_str = it.next() orelse {
+ // If no parameters are given, we reset the full table.
+ if (result.count() == 0) {
+ const req = try result.addOne(alloc);
+ req.* = switch (op) {
+ .osc_104 => .reset_palette,
+ .osc_105 => .reset_special,
+ else => comptime unreachable,
+ };
+ }
+ return result;
+ };
+
+ // Empty color strings are ignored, not treated as an error.
+ if (color_str.len == 0) continue;
+
+ // Color must be numeric. u9 because that'll fit our palette + special
+ const color: u9 = std.fmt.parseInt(
+ u9,
+ color_str,
+ 10,
+ ) catch continue;
+
+ // Parse the color.
+ const target: Target = switch (op) {
+ // OSC105 maps directly to the Special enum.
+ .osc_105 => .{ .special = std.meta.intToEnum(
+ SpecialColor,
+ std.math.cast(u3, color) orelse continue,
+ ) catch 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(
+ SpecialColor,
+ std.math.cast(u3, color - 256) orelse continue,
+ ) catch continue },
+
+ else => comptime unreachable,
+ };
+
+ const req = try result.addOne(alloc);
+ req.* = .{ .reset = target };
+ }
+}
+
+/// OSC 10-19: Get/Set Dynamic Colors
+fn parseGetSetDynamicColor(
+ alloc: Allocator,
+ start: DynamicColor,
+ it: *std.mem.TokenIterator(u8, .scalar),
+) Allocator.Error!List {
+ // Note: in ANY error scenario below we return the accumulated results.
+ // This matches the xterm behavior (see misc.c ChangeColorsRequest)
+
+ var result: List = .{};
+ var color: DynamicColor = start;
+ while (true) {
+ const spec_str = it.next() orelse return result;
+
+ if (std.mem.eql(u8, spec_str, "?")) {
+ const req = try result.addOne(alloc);
+ req.* = .{ .query = .{ .dynamic = color } };
+ } else {
+ const rgb = RGB.parse(spec_str) catch return result;
+ const req = try result.addOne(alloc);
+ req.* = .{ .set = .{
+ .target = .{ .dynamic = color },
+ .color = rgb,
+ } };
+ }
+
+ // Each successive value uses the next color so long as it exists.
+ color = color.next() orelse return result;
+ }
+}
+
+/// OSC 110-119: Reset Dynamic Colors
+fn parseResetDynamicColor(
+ alloc: Allocator,
+ color: DynamicColor,
+ it: *std.mem.TokenIterator(u8, .scalar),
+) Allocator.Error!List {
+ var result: List = .{};
+ errdefer result.deinit(alloc);
+ if (it.next() != null) return result;
+ const req = try result.addOne(alloc);
+ req.* = .{ .reset = .{ .dynamic = color } };
+ return result;
+}
+
+/// A segmented list is used to avoid copying when many operations
+/// are given in a single OSC. In most cases, OSC 4/104/etc. send
+/// very few so the prealloc is optimized for that.
+///
+/// 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(
+ Request,
+ 2,
+);
+
+/// A single operation related to the terminal color palette.
+pub const Request = union(enum) {
+ set: ColoredTarget,
+ query: Target,
+ reset: Target,
+ reset_palette,
+ reset_special,
+};
+
+pub const Target = union(enum) {
+ palette: u8,
+ special: SpecialColor,
+ dynamic: DynamicColor,
+};
+
+pub const ColoredTarget = struct {
+ target: Target,
+ color: RGB,
+};
+
+test "OSC 4:" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Test every palette index
+ for (0..std.math.maxInt(u8)) |idx| {
+ // Simple color set
+ // printf '\e]4;0;red\\'
+ {
+ const body = try std.fmt.allocPrint(
+ alloc,
+ "{d};red",
+ .{idx},
+ );
+ defer alloc.free(body);
+
+ var list = try parseColor(alloc, .osc_4, body);
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .palette = @intCast(idx) },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ }
+
+ // Simple color query
+ // printf '\e]4;0;?\\'
+ {
+ const body = try std.fmt.allocPrint(
+ alloc,
+ "{d};?",
+ .{idx},
+ );
+ defer alloc.free(body);
+
+ var list = try parseColor(alloc, .osc_4, body);
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .query = .{ .palette = @intCast(idx) } },
+ list.at(0).*,
+ );
+ }
+
+ // Trailing invalid data produces results up to that point
+ // printf '\e]4;0;red;\e\\'
+ {
+ const body = try std.fmt.allocPrint(
+ alloc,
+ "{d};red;",
+ .{idx},
+ );
+ defer alloc.free(body);
+
+ var list = try parseColor(alloc, .osc_4, body);
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .palette = @intCast(idx) },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ }
+
+ // Whitespace doesn't produce a working value in xterm but we
+ // allow it because Kitty does and it seems harmless.
+ //
+ // printf '\e]4;0;red \e\\'
+ {
+ const body = try std.fmt.allocPrint(
+ alloc,
+ "{d};red ",
+ .{idx},
+ );
+ defer alloc.free(body);
+
+ var list = try parseColor(alloc, .osc_4, body);
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .palette = @intCast(idx) },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ }
+ }
+
+ // Test every special color
+ for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
+ const special = try std.meta.intToEnum(SpecialColor, i);
+
+ // Simple color set
+ // printf '\e]4;256;red\\'
+ {
+ const body = try std.fmt.allocPrint(
+ alloc,
+ "{d};red",
+ .{256 + i},
+ );
+ defer alloc.free(body);
+
+ var list = try parseColor(alloc, .osc_4, body);
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .special = special },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ }
+ }
+}
+
+test "OSC 5:" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Test every special color
+ for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
+ const special = try std.meta.intToEnum(SpecialColor, i);
+
+ // Simple color set
+ // printf '\e]4;256;red\\'
+ {
+ const body = try std.fmt.allocPrint(
+ alloc,
+ "{d};red",
+ .{i},
+ );
+ defer alloc.free(body);
+
+ var list = try parseColor(alloc, .osc_5, body);
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .special = special },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ }
+ }
+}
+
+test "OSC 4: multiple requests" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // printf '\e]4;0;red;1;blue\e\\'
+ {
+ var list = try parseColor(
+ alloc,
+ .osc_4,
+ "0;red;1;blue",
+ );
+ defer list.deinit(alloc);
+ try testing.expectEqual(2, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .palette = 0 },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .palette = 1 },
+ .color = RGB{ .r = 0, .g = 0, .b = 255 },
+ } },
+ list.at(1).*,
+ );
+ }
+
+ // Multiple requests with same index overwrite each other
+ // printf '\e]4;0;red;0;blue\e\\'
+ {
+ var list = try parseColor(
+ alloc,
+ .osc_4,
+ "0;red;0;blue",
+ );
+ defer list.deinit(alloc);
+ try testing.expectEqual(2, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .palette = 0 },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .palette = 0 },
+ .color = RGB{ .r = 0, .g = 0, .b = 255 },
+ } },
+ list.at(1).*,
+ );
+ }
+}
+
+test "OSC 104:" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Test every palette index
+ for (0..std.math.maxInt(u8)) |idx| {
+ // Simple color set
+ // printf '\e]104;0\\'
+ {
+ const body = try std.fmt.allocPrint(
+ alloc,
+ "{d}",
+ .{idx},
+ );
+ defer alloc.free(body);
+
+ var list = try parseColor(alloc, .osc_104, body);
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .reset = .{ .palette = @intCast(idx) } },
+ list.at(0).*,
+ );
+ }
+ }
+
+ // Test every special color
+ for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
+ const special = try std.meta.intToEnum(SpecialColor, i);
+
+ // Simple color set
+ // printf '\e]104;256\\'
+ {
+ const body = try std.fmt.allocPrint(
+ alloc,
+ "{d}",
+ .{256 + i},
+ );
+ defer alloc.free(body);
+
+ var list = try parseColor(alloc, .osc_104, body);
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .reset = .{ .special = special } },
+ list.at(0).*,
+ );
+ }
+ }
+}
+
+test "OSC 104: empty index" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var list = try parseColor(alloc, .osc_104, "0;;1");
+ defer list.deinit(alloc);
+ try testing.expectEqual(2, list.count());
+ try testing.expectEqual(
+ Request{ .reset = .{ .palette = 0 } },
+ list.at(0).*,
+ );
+ try testing.expectEqual(
+ Request{ .reset = .{ .palette = 1 } },
+ list.at(1).*,
+ );
+}
+
+test "OSC 104: invalid index" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var list = try parseColor(alloc, .osc_104, "ffff;1");
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .reset = .{ .palette = 1 } },
+ list.at(0).*,
+ );
+}
+
+test "OSC 104: reset all" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var list = try parseColor(alloc, .osc_104, "");
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .reset_palette = {} },
+ list.at(0).*,
+ );
+}
+
+test "OSC 105: reset all" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var list = try parseColor(alloc, .osc_105, "");
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .reset_special = {} },
+ list.at(0).*,
+ );
+}
+
+// OSC 10-19: Get/Set Dynamic Colors
+test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: OSC 19: dynamic" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ inline for (@typeInfo(DynamicColor).@"enum".fields) |field| {
+ const color = @field(DynamicColor, field.name);
+ const op = @field(Operation, std.fmt.comptimePrint(
+ "osc_{d}",
+ .{field.value},
+ ));
+
+ // Example script:
+ // printf '\e]10;red\e\\'
+ {
+ var list = try parseColor(alloc, op, "red");
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .dynamic = color },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ }
+ }
+}
+
+test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: OSC 19: dynamic multiple" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Example script:
+ // printf '\e]11;red;blue\e\\'
+ {
+ var list = try parseColor(
+ alloc,
+ .osc_11,
+ "red;blue",
+ );
+ defer list.deinit(alloc);
+ try testing.expectEqual(2, list.count());
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .dynamic = .background },
+ .color = RGB{ .r = 255, .g = 0, .b = 0 },
+ } },
+ list.at(0).*,
+ );
+ try testing.expectEqual(
+ Request{ .set = .{
+ .target = .{ .dynamic = .cursor },
+ .color = RGB{ .r = 0, .g = 0, .b = 255 },
+ } },
+ list.at(1).*,
+ );
+ }
+}
+
+// OSC 110-119: Reset Dynamic Colors
+test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: OSC 118: OSC 119: reset dynamic" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ inline for (@typeInfo(DynamicColor).@"enum".fields) |field| {
+ const color = @field(DynamicColor, field.name);
+ const op = @field(Operation, std.fmt.comptimePrint(
+ "osc_1{d}",
+ .{field.value},
+ ));
+
+ // Example script:
+ // printf '\e]110\e\\'
+ {
+ var list = try parseColor(alloc, op, "");
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .reset = .{ .dynamic = color } },
+ list.at(0).*,
+ );
+ }
+
+ // xterm allows a trailing semicolon. script to verify:
+ //
+ // printf '\e]110;\e\\'
+ {
+ var list = try parseColor(alloc, op, ";");
+ defer list.deinit(alloc);
+ try testing.expectEqual(1, list.count());
+ try testing.expectEqual(
+ Request{ .reset = .{ .dynamic = color } },
+ list.at(0).*,
+ );
+ }
+
+ // xterm does NOT allow any whitespace
+ //
+ // printf '\e]110 \e\\'
+ {
+ var list = try parseColor(alloc, op, " ");
+ defer list.deinit(alloc);
+ try testing.expectEqual(0, list.count());
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/context_signal.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/context_signal.zig
new file mode 100644
index 00000000..e7eb9348
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/context_signal.zig
@@ -0,0 +1,541 @@
+//! OSC 3008: Hierarchical Context Signalling (UAPI spec)
+//! Specification: https://uapi-group.org/specifications/specs/osc_context/
+//!
+//! OSC 3008 allows programs to signal context changes to the terminal emulator.
+//! Each context has an identifier and metadata fields. Contexts are hierarchical
+//! and form a stack.
+
+const std = @import("std");
+const Parser = @import("../../osc.zig").Parser;
+const OSCCommand = @import("../../osc.zig").Command;
+
+const log = std.log.scoped(.osc_context_signal);
+
+/// Maximum length of a context identifier (per spec).
+const max_context_id_len = 64;
+
+/// A single OSC 3008 context signal command.
+pub const Command = struct {
+ pub const C = void;
+
+ action: Action,
+ /// The context identifier. Must be 1-64 characters in the 32..126 byte range.
+ id: []const u8,
+ /// Raw unparsed metadata fields after the context ID.
+ /// Fields are semicolon-separated key=value pairs.
+ /// Parsed lazily via `readOption`.
+ metadata: []const u8,
+
+ pub const Action = enum {
+ /// OSC 3008;start=<id> — initiates, updates, or returns to a context.
+ start,
+ /// OSC 3008;end=<id> — terminates a context.
+ end,
+ };
+
+ /// Read a metadata field value from the raw fields string.
+ /// Returns null if the field is not present or malformed.
+ pub fn readOption(
+ self: Command,
+ comptime option: Field,
+ ) option.Type() {
+ return option.read(self.metadata);
+ }
+};
+
+/// Context types defined by the specification.
+pub const ContextType = enum {
+ boot,
+ container,
+ vm,
+ elevate,
+ chpriv,
+ subcontext,
+ remote,
+ shell,
+ command,
+ app,
+ service,
+ session,
+
+ pub fn parse(value: []const u8) ?ContextType {
+ return std.meta.stringToEnum(ContextType, value);
+ }
+};
+
+/// Exit status for the `exit` end-sequence field.
+pub const ExitStatus = enum {
+ success,
+ failure,
+ crash,
+ interrupt,
+
+ pub fn parse(value: []const u8) ?ExitStatus {
+ return std.meta.stringToEnum(ExitStatus, value);
+ }
+};
+
+/// Metadata fields that can appear in OSC 3008 sequences.
+/// Fields are read lazily from the raw string using the `read` method.
+pub const Field = enum {
+ // Start sequence fields
+ type,
+ user,
+ hostname,
+ machineid,
+ bootid,
+ pid,
+ pidfdid,
+ comm,
+ cwd,
+ cmdline,
+ vm,
+ container,
+ targetuser,
+ targethost,
+ sessionid,
+
+ // End sequence fields
+ exit,
+ status,
+ signal,
+
+ pub fn Type(comptime self: Field) type {
+ return switch (self) {
+ .type => ?ContextType,
+ .exit => ?ExitStatus,
+ .pid, .pidfdid => ?u64,
+ .status => ?u64,
+ // All other fields are string values
+ .user,
+ .hostname,
+ .machineid,
+ .bootid,
+ .comm,
+ .cwd,
+ .cmdline,
+ .vm,
+ .container,
+ .targetuser,
+ .targethost,
+ .sessionid,
+ .signal,
+ => ?[]const u8,
+ };
+ }
+
+ fn key(comptime self: Field) []const u8 {
+ return @tagName(self);
+ }
+
+ /// Read the field value from the raw fields string.
+ ///
+ /// The raw fields string contains semicolon-separated key=value pairs
+ /// e.g. "type=container;user=lennart;hostname=zeta".
+ ///
+ /// Unknown or malformed fields are ignored per the specification.
+ pub fn read(
+ comptime self: Field,
+ raw: []const u8,
+ ) self.Type() {
+ var it = std.mem.splitScalar(u8, raw, ';');
+ while (it.next()) |full| {
+ // Parse key=value
+ const value = value: {
+ if (std.mem.indexOfScalar(
+ u8,
+ full,
+ '=',
+ )) |eql_idx| {
+ if (std.mem.eql(
+ u8,
+ full[0..eql_idx],
+ self.key(),
+ )) {
+ break :value full[eql_idx + 1 ..];
+ }
+ }
+
+ continue;
+ };
+
+ return switch (self) {
+ .type => .parse(value),
+ .exit => .parse(value),
+ .pid, .pidfdid, .status => value: {
+ for (value) |c| {
+ if (c < '0' or c > '9') break :value null;
+ }
+ break :value std.fmt.parseInt(
+ u64,
+ value,
+ 10,
+ ) catch null;
+ },
+ // String fields
+ .user,
+ .hostname,
+ .machineid,
+ .bootid,
+ .comm,
+ .cwd,
+ .cmdline,
+ .vm,
+ .container,
+ .targetuser,
+ .targethost,
+ .sessionid,
+ .signal,
+ => if (value.len > 0) value else null,
+ };
+ }
+
+ // Not found
+ return null;
+ }
+};
+
+/// Parse OSC 3008: hierarchical context signalling.
+///
+/// Expected data format (after "3008;" prefix has been consumed by the state machine):
+/// start=<id>[;<field>=<value>]*
+/// end=<id>[;<field>=<value>]*
+pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand {
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ if (data.len == 0) {
+ parser.state = .invalid;
+ return null;
+ }
+
+ // Determine the action (start= or end=)
+ const action: Command.Action = action: {
+ if (std.mem.startsWith(u8, data, "start=")) break :action .start;
+ if (std.mem.startsWith(u8, data, "end=")) break :action .end;
+
+ log.warn("OSC 3008: expected 'start=' or 'end=' prefix, got: {s}", .{
+ data[0..@min(data.len, 10)],
+ });
+ parser.state = .invalid;
+ return null;
+ };
+
+ // Skip past the "start=" or "end=" prefix
+ const prefix_len: usize = switch (action) {
+ .start => "start=".len,
+ .end => "end=".len,
+ };
+ const rest = data[prefix_len..];
+
+ if (rest.len == 0) {
+ log.warn("OSC 3008: missing context ID", .{});
+ parser.state = .invalid;
+ return null;
+ }
+
+ // Extract the context ID (up to the first semicolon or end of data)
+ const id_end = std.mem.indexOfScalar(u8, rest, ';') orelse rest.len;
+ const id = rest[0..id_end];
+
+ // Validate context ID length (1-64 chars per spec)
+ if (id.len == 0 or id.len > max_context_id_len) {
+ log.warn("OSC 3008: context ID length {d} out of range (1-{d})", .{
+ id.len,
+ max_context_id_len,
+ });
+ parser.state = .invalid;
+ return null;
+ }
+
+ // Validate context ID characters (32..126 byte range per spec)
+ for (id) |c| {
+ if (c < 0x20 or c > 0x7e) {
+ log.warn("OSC 3008: invalid character 0x{x:0>2} in context ID", .{c});
+ parser.state = .invalid;
+ return null;
+ }
+ }
+
+ // Extract raw metadata fields (everything after the ID)
+ const metadata = if (id_end < rest.len) rest[id_end + 1 ..] else "";
+
+ parser.command = .{
+ .context_signal = .{
+ .action = action,
+ .id = id,
+ .metadata = metadata,
+ },
+ };
+
+ return &parser.command;
+}
+
+// ============================================================================
+// Tests
+// ============================================================================
+
+test "OSC 3008: basic start command" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;start=abc123";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.action == .start);
+ try testing.expectEqualStrings("abc123", cmd.context_signal.id);
+ try testing.expectEqualStrings("", cmd.context_signal.metadata);
+}
+
+test "OSC 3008: basic end command" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;end=abc123";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.action == .end);
+ try testing.expectEqualStrings("abc123", cmd.context_signal.id);
+ try testing.expectEqualStrings("", cmd.context_signal.metadata);
+}
+
+test "OSC 3008: start with metadata fields" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;start=bed86fab93af4328bbed0a1224af6d40;type=container;user=lennart;hostname=zeta";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.action == .start);
+ try testing.expectEqualStrings("bed86fab93af4328bbed0a1224af6d40", cmd.context_signal.id);
+
+ // Read individual fields
+ try testing.expect(cmd.context_signal.readOption(.type).? == .container);
+ try testing.expectEqualStrings("lennart", cmd.context_signal.readOption(.user).?);
+ try testing.expectEqualStrings("zeta", cmd.context_signal.readOption(.hostname).?);
+}
+
+test "OSC 3008: start with all common fields" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;start=myctx;type=shell;user=root;hostname=myhost;machineid=3deb5353d3ba43d08201c136a47ead7b;bootid=d4a3d0fdf2e24fdea6d971ce73f4fbf2;pid=1062862;pidfdid=1063162;comm=bash";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.readOption(.type).? == .shell);
+ try testing.expectEqualStrings("root", cmd.context_signal.readOption(.user).?);
+ try testing.expectEqualStrings("myhost", cmd.context_signal.readOption(.hostname).?);
+ try testing.expectEqualStrings("3deb5353d3ba43d08201c136a47ead7b", cmd.context_signal.readOption(.machineid).?);
+ try testing.expectEqualStrings("d4a3d0fdf2e24fdea6d971ce73f4fbf2", cmd.context_signal.readOption(.bootid).?);
+ try testing.expectEqual(@as(u64, 1062862), cmd.context_signal.readOption(.pid).?);
+ try testing.expectEqual(@as(u64, 1063162), cmd.context_signal.readOption(.pidfdid).?);
+ try testing.expectEqualStrings("bash", cmd.context_signal.readOption(.comm).?);
+}
+
+test "OSC 3008: end with exit metadata" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;end=myctx;exit=success;status=0";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.action == .end);
+ try testing.expectEqualStrings("myctx", cmd.context_signal.id);
+ try testing.expect(cmd.context_signal.readOption(.exit).? == .success);
+ try testing.expectEqual(@as(u64, 0), cmd.context_signal.readOption(.status).?);
+}
+
+test "OSC 3008: end with failure exit" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;end=myctx;exit=failure;status=1;signal=SIGKILL";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.readOption(.exit).? == .failure);
+ try testing.expectEqual(@as(u64, 1), cmd.context_signal.readOption(.status).?);
+ try testing.expectEqualStrings("SIGKILL", cmd.context_signal.readOption(.signal).?);
+}
+
+test "OSC 3008: unknown fields are ignored" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;start=myctx;type=shell;unknownfield=value;user=root";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.readOption(.type).? == .shell);
+ try testing.expectEqualStrings("root", cmd.context_signal.readOption(.user).?);
+}
+
+test "OSC 3008: missing field returns null" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;start=myctx;user=lennart";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.readOption(.type) == null);
+ try testing.expect(cmd.context_signal.readOption(.hostname) == null);
+ try testing.expect(cmd.context_signal.readOption(.pid) == null);
+}
+
+test "OSC 3008: invalid prefix" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;bogus=abc123";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 3008: empty data" {
+ const testing = std.testing;
+
+ // Can't really produce empty data after "3008;" because the state machine
+ // won't write a writer for that case, but we test the edge case where
+ // only "start=" is present with no ID.
+ var p: Parser = .init(null);
+ const input = "3008;start=";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 3008: max length context ID" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const id = "a" ** 64;
+ const input = "3008;start=" ++ id;
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expectEqualStrings(id, cmd.context_signal.id);
+}
+
+test "OSC 3008: over-length context ID" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const id = "a" ** 65;
+ const input = "3008;start=" ++ id;
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 3008: context type enum coverage" {
+ const testing = std.testing;
+
+ const types = [_]struct { str: []const u8, expected: ContextType }{
+ .{ .str = "boot", .expected = .boot },
+ .{ .str = "container", .expected = .container },
+ .{ .str = "vm", .expected = .vm },
+ .{ .str = "elevate", .expected = .elevate },
+ .{ .str = "chpriv", .expected = .chpriv },
+ .{ .str = "subcontext", .expected = .subcontext },
+ .{ .str = "remote", .expected = .remote },
+ .{ .str = "shell", .expected = .shell },
+ .{ .str = "command", .expected = .command },
+ .{ .str = "app", .expected = .app },
+ .{ .str = "service", .expected = .service },
+ .{ .str = "session", .expected = .session },
+ };
+
+ for (types) |t| {
+ try testing.expectEqual(t.expected, ContextType.parse(t.str).?);
+ }
+
+ try testing.expect(ContextType.parse("invalid") == null);
+}
+
+test "OSC 3008: exit status enum coverage" {
+ const testing = std.testing;
+
+ try testing.expect(ExitStatus.parse("success").? == .success);
+ try testing.expect(ExitStatus.parse("failure").? == .failure);
+ try testing.expect(ExitStatus.parse("crash").? == .crash);
+ try testing.expect(ExitStatus.parse("interrupt").? == .interrupt);
+ try testing.expect(ExitStatus.parse("invalid") == null);
+}
+
+test "OSC 3008: spec example - container start" {
+ const testing = std.testing;
+
+ // From the spec: a new container "foobar" invoked by user "lennart" on host "zeta"
+ var p: Parser = .init(null);
+ const input = "3008;start=bed86fab93af4328bbed0a1224af6d40;type=container;user=lennart;hostname=zeta;machineid=3deb5353d3ba43d08201c136a47ead7b;bootid=d4a3d0fdf2e24fdea6d971ce73f4fbf2;pid=1062862;pidfdid=1063162;comm=systemd-nspawn;container=foobar";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.action == .start);
+ try testing.expectEqualStrings("bed86fab93af4328bbed0a1224af6d40", cmd.context_signal.id);
+ try testing.expect(cmd.context_signal.readOption(.type).? == .container);
+ try testing.expectEqualStrings("lennart", cmd.context_signal.readOption(.user).?);
+ try testing.expectEqualStrings("zeta", cmd.context_signal.readOption(.hostname).?);
+ try testing.expectEqualStrings("systemd-nspawn", cmd.context_signal.readOption(.comm).?);
+ try testing.expectEqualStrings("foobar", cmd.context_signal.readOption(.container).?);
+ try testing.expectEqual(@as(u64, 1062862), cmd.context_signal.readOption(.pid).?);
+}
+
+test "OSC 3008: spec example - context end" {
+ const testing = std.testing;
+
+ // From the spec: context end
+ var p: Parser = .init(null);
+ const input = "3008;end=bed86fab93af4328bbed0a1224af6d40";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.action == .end);
+ try testing.expectEqualStrings("bed86fab93af4328bbed0a1224af6d40", cmd.context_signal.id);
+}
+
+test "OSC 3008: cwd and cmdline fields" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;start=myctx;type=command;cwd=/home/user;cmdline=ls -la";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expectEqualStrings("/home/user", cmd.context_signal.readOption(.cwd).?);
+ try testing.expectEqualStrings("ls -la", cmd.context_signal.readOption(.cmdline).?);
+}
+
+test "OSC 3008: start command with no fields" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "3008;start=simpleid";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .context_signal);
+ try testing.expect(cmd.context_signal.action == .start);
+ try testing.expectEqualStrings("simpleid", cmd.context_signal.id);
+ try testing.expect(cmd.context_signal.readOption(.type) == null);
+ try testing.expect(cmd.context_signal.readOption(.user) == null);
+ try testing.expect(cmd.context_signal.readOption(.exit) == null);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/hyperlink.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/hyperlink.zig
new file mode 100644
index 00000000..d8e84b63
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/hyperlink.zig
@@ -0,0 +1,164 @@
+const std = @import("std");
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+const log = std.log.scoped(.osc_hyperlink);
+
+/// Parse OSC 8 hyperlinks
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ cap.writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ const s = std.mem.indexOfScalar(u8, data, ';') orelse {
+ parser.state = .invalid;
+ return null;
+ };
+
+ parser.command = .{
+ .hyperlink_start = .{
+ .uri = data[s + 1 .. data.len - 1 :0],
+ },
+ };
+
+ data[s] = 0;
+ const kvs = data[0 .. s + 1];
+ std.mem.replaceScalar(u8, kvs, ':', 0);
+ var kv_start: usize = 0;
+ while (kv_start < kvs.len) {
+ const kv_end = std.mem.indexOfScalarPos(u8, kvs, kv_start + 1, 0) orelse break;
+ const kv = data[kv_start .. kv_end + 1];
+ const v = std.mem.indexOfScalar(u8, kv, '=') orelse break;
+ const key = kv[0..v];
+ const value = kv[v + 1 .. kv.len - 1 :0];
+ if (std.mem.eql(u8, key, "id")) {
+ if (value.len > 0) parser.command.hyperlink_start.id = value;
+ } else {
+ log.warn("unknown hyperlink option: '{s}'", .{key});
+ }
+ kv_start = kv_end + 1;
+ }
+
+ if (parser.command.hyperlink_start.uri.len == 0) {
+ if (parser.command.hyperlink_start.id != null) {
+ parser.state = .invalid;
+ return null;
+ }
+ parser.command = .hyperlink_end;
+ }
+
+ return &parser.command;
+}
+
+test "OSC 8: hyperlink" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "8;;http://example.com";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .hyperlink_start);
+ try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
+}
+
+test "OSC 8: hyperlink with id set" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "8;id=foo;http://example.com";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .hyperlink_start);
+ try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo");
+ try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
+}
+
+test "OSC 8: hyperlink with empty id" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "8;id=;http://example.com";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .hyperlink_start);
+ try testing.expectEqual(null, cmd.hyperlink_start.id);
+ try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
+}
+
+test "OSC 8: hyperlink with incomplete key" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "8;id;http://example.com";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .hyperlink_start);
+ try testing.expectEqual(null, cmd.hyperlink_start.id);
+ try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
+}
+
+test "OSC 8: hyperlink with empty key" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "8;=value;http://example.com";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .hyperlink_start);
+ try testing.expectEqual(null, cmd.hyperlink_start.id);
+ try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
+}
+
+test "OSC 8: hyperlink with empty key and id" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "8;=value:id=foo;http://example.com";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .hyperlink_start);
+ try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo");
+ try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
+}
+
+test "OSC 8: hyperlink with empty uri" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "8;id=foo;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b');
+ try testing.expect(cmd == null);
+}
+
+test "OSC 8: hyperlink end" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "8;;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .hyperlink_end);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/iterm2.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/iterm2.zig
new file mode 100644
index 00000000..b05dd1b2
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/iterm2.zig
@@ -0,0 +1,435 @@
+const std = @import("std");
+
+const assert = @import("../../../quirks.zig").inlineAssert;
+const simd = @import("../../../simd/main.zig");
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+const log = std.log.scoped(.osc_iterm2);
+
+const Key = enum {
+ AddAnnotation,
+ AddHiddenAnnotation,
+ Block,
+ Button,
+ ClearCapturedOutput,
+ ClearScrollback,
+ Copy,
+ CopyToClipboard,
+ CurrentDir,
+ CursorShape,
+ Custom,
+ Disinter,
+ EndCopy,
+ File,
+ FileEnd,
+ FilePart,
+ HighlightCursorLine,
+ MultipartFile,
+ OpenURL,
+ PopKeyLabels,
+ PushKeyLabels,
+ RemoteHost,
+ ReportCellSize,
+ ReportVariable,
+ RequestAttention,
+ RequestUpload,
+ SetBackgroundImageFile,
+ SetBadgeFormat,
+ SetColors,
+ SetKeyLabel,
+ SetMark,
+ SetProfile,
+ SetUserVar,
+ ShellIntegrationVersion,
+ StealFocus,
+ UnicodeVersion,
+};
+
+// Instead of using `std.meta.stringToEnum` we set up a StaticStringMap so
+// that we can get ASCII case-insensitive lookups.
+const Map = std.StaticStringMapWithEql(Key, std.ascii.eqlIgnoreCase);
+const map: Map = .initComptime(
+ map: {
+ const fields = @typeInfo(Key).@"enum".fields;
+ var tmp: [fields.len]struct { [:0]const u8, Key } = undefined;
+ for (fields, 0..) |field, i| {
+ tmp[i] = .{ field.name, @enumFromInt(field.value) };
+ }
+ break :map tmp;
+ },
+);
+
+/// Parse OSC 1337
+/// https://iterm2.com/documentation-escape-codes.html
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ assert(parser.state == .@"1337");
+
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ cap.writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+
+ const key_str: [:0]u8, const value_: ?[:0]u8 = kv: {
+ const index = std.mem.indexOfScalar(u8, data, '=') orelse {
+ break :kv .{ data[0 .. data.len - 1 :0], null };
+ };
+ data[index] = 0;
+ break :kv .{ data[0..index :0], data[index + 1 .. data.len - 1 :0] };
+ };
+
+ const key = map.get(key_str) orelse {
+ parser.command = .invalid;
+ return null;
+ };
+
+ switch (key) {
+ .Copy => {
+ var value = value_ orelse {
+ parser.command = .invalid;
+ return null;
+ };
+
+ // Sending a blank entry to clear the clipboard is an OSC 52-ism,
+ // make sure that is invalid here.
+ if (value.len == 0) {
+ parser.command = .invalid;
+ return null;
+ }
+
+ // base64 value must be prefixed by a colon
+ if (value[0] != ':') {
+ parser.command = .invalid;
+ return null;
+ }
+
+ value = value[1..value.len :0];
+
+ // Sending a blank entry to clear the clipboard is an OSC 52-ism,
+ // make sure that is invalid here.
+ if (value.len == 0) {
+ parser.command = .invalid;
+ return null;
+ }
+
+ // Sending a '?' to query the clipboard is an OSC 52-ism, make sure
+ // that is invalid here.
+ if (value.len == 1 and value[0] == '?') {
+ parser.command = .invalid;
+ return null;
+ }
+
+ // It would be better to check for valid base64 data here, but that
+ // would mean parsing the base64 data twice in the "normal" case.
+
+ parser.command = .{
+ .clipboard_contents = .{
+ .kind = 'c',
+ .data = value,
+ },
+ };
+ return &parser.command;
+ },
+
+ .CurrentDir => {
+ const value = value_ orelse {
+ parser.command = .invalid;
+ return null;
+ };
+ if (value.len == 0) {
+ parser.command = .invalid;
+ return null;
+ }
+ parser.command = .{
+ .report_pwd = .{
+ .value = value,
+ },
+ };
+ return &parser.command;
+ },
+
+ .AddAnnotation,
+ .AddHiddenAnnotation,
+ .Block,
+ .Button,
+ .ClearCapturedOutput,
+ .ClearScrollback,
+ .CopyToClipboard,
+ .CursorShape,
+ .Custom,
+ .Disinter,
+ .EndCopy,
+ .File,
+ .FileEnd,
+ .FilePart,
+ .HighlightCursorLine,
+ .MultipartFile,
+ .OpenURL,
+ .PopKeyLabels,
+ .PushKeyLabels,
+ .RemoteHost,
+ .ReportCellSize,
+ .ReportVariable,
+ .RequestAttention,
+ .RequestUpload,
+ .SetBackgroundImageFile,
+ .SetBadgeFormat,
+ .SetColors,
+ .SetKeyLabel,
+ .SetMark,
+ .SetProfile,
+ .SetUserVar,
+ .ShellIntegrationVersion,
+ .StealFocus,
+ .UnicodeVersion,
+ => {
+ log.debug("unimplemented OSC 1337: {t}", .{key});
+ parser.command = .invalid;
+ return null;
+ },
+ }
+ return &parser.command;
+}
+
+test "OSC: 1337: test valid unimplemented key with no value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;SetBadgeFormat";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test valid unimplemented key with empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;SetBadgeFormat=";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test valid unimplemented key with non-empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;SetBadgeFormat=abc123";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test valid key with lower case and with no value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;setbadgeformat";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test valid key with lower case and with empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;setbadgeformat=";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test valid key with lower case and with non-empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;setbadgeformat=abc123";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test invalid key with no value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;BobrKurwa";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test invalid key with empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;BobrKurwa=";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test invalid key with non-empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;BobrKurwa=abc123";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test Copy with no value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;Copy";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test Copy with empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;Copy=";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test Copy with only prefix colon" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;Copy=:";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test Copy with question mark" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;Copy=:?";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test Copy with non-empty value that is invalid base64" {
+ // For performance reasons, we don't check for valid base64 data
+ // right now.
+ return error.SkipZigTest;
+
+ // const testing = std.testing;
+
+ // var p: Parser = .init(testing.allocator);
+ // defer p.deinit();
+
+ // const input = "1337;Copy=:abc123";
+ // for (input) |ch| p.next(ch);
+
+ // try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test Copy with non-empty value that is valid base64 but not prefixed with a colon" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;Copy=YWJjMTIz";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test Copy with non-empty value that is valid base64" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;Copy=:YWJjMTIz";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .clipboard_contents);
+ try testing.expectEqual('c', cmd.clipboard_contents.kind);
+ try testing.expectEqualStrings("YWJjMTIz", cmd.clipboard_contents.data);
+}
+
+test "OSC: 1337: test CurrentDir with no value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;CurrentDir";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test CurrentDir with empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;CurrentDir=";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC: 1337: test CurrentDir with non-empty value" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "1337;CurrentDir=abc123";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .report_pwd);
+ try testing.expectEqualStrings("abc123", cmd.report_pwd.value);
+}
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
new file mode 100644
index 00000000..5bd0e254
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_clipboard_protocol.zig
@@ -0,0 +1,702 @@
+//! Kitty's clipboard protocol (OSC 5522)
+//! Specification: https://sw.kovidgoyal.net/kitty/clipboard/
+//! https://rockorager.dev/misc/bracketed-paste-mime/
+
+const std = @import("std");
+const build_options = @import("terminal_options");
+
+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 encoding = @import("../encoding.zig");
+
+const log = std.log.scoped(.kitty_clipboard_protocol);
+
+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 Location = enum {
+ primary,
+
+ pub fn init(str: []const u8) ?Location {
+ return std.meta.stringToEnum(Location, str);
+ }
+};
+
+pub const Operation = enum {
+ read,
+ walias,
+ wdata,
+ write,
+
+ pub fn init(str: []const u8) ?Operation {
+ return std.meta.stringToEnum(Operation, str);
+ }
+};
+
+pub const Status = enum {
+ DATA,
+ DONE,
+ EBUSY,
+ EINVAL,
+ EIO,
+ ENOSYS,
+ EPERM,
+ OK,
+
+ pub fn init(str: []const u8) ?Status {
+ return std.meta.stringToEnum(Status, str);
+ }
+};
+
+pub const Option = enum {
+ id,
+ loc,
+ mime,
+ name,
+ password,
+ pw,
+ status,
+ type,
+
+ pub fn Type(comptime key: Option) type {
+ return switch (key) {
+ .id => []const u8,
+ .loc => Location,
+ .mime => []const u8,
+ .name => []const u8,
+ .password => []const u8,
+ .pw => []const u8,
+ .status => Status,
+ .type => Operation,
+ };
+ }
+
+ /// Read the option value from the raw metadata string.
+ pub fn read(
+ 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;
+ };
+
+ // return the parsed value
+ return switch (key) {
+ .id => parseIdentifier(value),
+ .loc => .init(value),
+ .mime => value,
+ .name => value,
+ .password => value,
+ .pw => value,
+ .status => .init(value),
+ .type => .init(value),
+ };
+ }
+};
+
+/// Characters that are valid in identifiers.
+const valid_identifier_characters: []const u8 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+.";
+
+fn isValidIdentifier(str: []const u8) bool {
+ if (str.len == 0) return false;
+ 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 == .@"5522");
+
+ 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 start = std.mem.indexOfScalar(u8, data, ';') orelse break :result .{ data, null };
+ break :result .{ data[0..start], data[start + 1 .. data.len] };
+ };
+
+ parser.command = .{
+ .kitty_clipboard_protocol = .{
+ .metadata = metadata,
+ .payload = payload,
+ .terminator = .init(terminator_ch),
+ },
+ };
+
+ return &parser.command;
+}
+
+test "OSC: 5522: empty metadata and missing payload" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("", cmd.kitty_clipboard_protocol.metadata);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.type) == null);
+}
+
+test "OSC: 5522: empty metadata and empty payload" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("", cmd.kitty_clipboard_protocol.metadata);
+ try testing.expectEqualStrings("", cmd.kitty_clipboard_protocol.payload.?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.type) == null);
+}
+
+test "OSC: 5522: non-empty metadata and payload" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read;dGV4dC9wbGFpbg==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("type=read", cmd.kitty_clipboard_protocol.metadata);
+ try testing.expectEqualStrings("dGV4dC9wbGFpbg==", cmd.kitty_clipboard_protocol.payload.?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type));
+}
+
+test "OSC: 5522: empty id" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;id=";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+}
+
+test "OSC: 5522: valid id" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;id=5c076ad9-d36f-4705-847b-d4dbf356cc0d";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("5c076ad9-d36f-4705-847b-d4dbf356cc0d", cmd.kitty_clipboard_protocol.readOption(.id).?);
+}
+
+test "OSC: 5522: invalid id" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;id=*42*";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+}
+
+test "OSC: 5522: invalid status" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;status=BOBR";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+}
+
+test "OSC: 5522: valid status" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;status=DONE";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqual(.DONE, cmd.kitty_clipboard_protocol.readOption(.status).?);
+}
+
+test "OSC: 5522: invalid location" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;loc=bobr";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+}
+
+test "OSC: 5522: valid location" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;loc=primary";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqual(.primary, cmd.kitty_clipboard_protocol.readOption(.loc).?);
+}
+
+test "OSC: 5522: password 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;pw=R2hvc3R0eQ==:name=Qk9CUiBLVVJXQQ==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_clipboard_protocol.readOption(.pw).?);
+ try testing.expectEqualStrings("Qk9CUiBLVVJXQQ==", cmd.kitty_clipboard_protocol.readOption(.name).?);
+}
+
+test "OSC: 5522: password 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;password=R2hvc3R0eQ==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_clipboard_protocol.readOption(.password).?);
+}
+
+test "OSC: 5522: example 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:status=OK";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.OK, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:mime=dGV4dC9wbGFpbg==;R2hvc3R0eQ==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_clipboard_protocol.payload.?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expectEqualStrings("dGV4dC9wbGFpbg==", cmd.kitty_clipboard_protocol.readOption(.mime).?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 3" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:status=OK";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.OK, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 4" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=write";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expectEqual(.write, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 5" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=wdata:mime=dGV4dC9wbGFpbg==;R2hvc3R0eQ==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_clipboard_protocol.payload.?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expectEqualStrings("dGV4dC9wbGFpbg==", cmd.kitty_clipboard_protocol.readOption(.mime).?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expectEqual(.wdata, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 6" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=wdata";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expectEqual(.wdata, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 7" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=write:status=DONE";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.DONE, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.write, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 8" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=write:status=EPERM";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.EPERM, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.write, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 9" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=walias:mime=dGV4dC9wbGFpbg==;dGV4dC9odG1sIGFwcGxpY2F0aW9uL2pzb24=";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("dGV4dC9odG1sIGFwcGxpY2F0aW9uL2pzb24=", cmd.kitty_clipboard_protocol.payload.?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expectEqualStrings("dGV4dC9wbGFpbg==", cmd.kitty_clipboard_protocol.readOption(.mime).?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expectEqual(.walias, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 10" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:status=OK:password=Qk9CUiBLVVJXQQ==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expectEqualStrings("Qk9CUiBLVVJXQQ==", cmd.kitty_clipboard_protocol.readOption(.password).?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.OK, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 11" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:status=DATA:mime=dGV4dC9wbGFpbg==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expectEqualStrings("dGV4dC9wbGFpbg==", cmd.kitty_clipboard_protocol.readOption(.mime).?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.DATA, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 12" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:mime=dGV4dC9wbGFpbg==:password=Qk9CUiBLVVJXQQ==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expectEqualStrings("dGV4dC9wbGFpbg==", cmd.kitty_clipboard_protocol.readOption(.mime).?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expectEqualStrings("Qk9CUiBLVVJXQQ==", cmd.kitty_clipboard_protocol.readOption(.password).?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.status) == null);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 13" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:status=OK";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.OK, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 14" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:status=DATA:mime=dGV4dC9wbGFpbg==;Qk9CUiBLVVJXQQ==";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expectEqualStrings("Qk9CUiBLVVJXQQ==", cmd.kitty_clipboard_protocol.payload.?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expectEqualStrings("dGV4dC9wbGFpbg==", cmd.kitty_clipboard_protocol.readOption(.mime).?);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.DATA, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
+
+test "OSC: 5522: example 15" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "5522;type=read:status=OK";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_clipboard_protocol);
+ try testing.expect(cmd.kitty_clipboard_protocol.payload == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.id) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.loc) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.mime) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.name) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.password) == null);
+ try testing.expect(cmd.kitty_clipboard_protocol.readOption(.pw) == null);
+ try testing.expectEqual(.OK, cmd.kitty_clipboard_protocol.readOption(.status).?);
+ try testing.expectEqual(.read, cmd.kitty_clipboard_protocol.readOption(.type).?);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_color.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_color.zig
new file mode 100644
index 00000000..c59391ea
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_color.zig
@@ -0,0 +1,212 @@
+const std = @import("std");
+
+const assert = @import("../../../quirks.zig").inlineAssert;
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+const kitty_color = @import("../../kitty/color.zig");
+const RGB = @import("../../color.zig").RGB;
+
+const log = std.log.scoped(.osc_kitty_color);
+
+/// Parse OSC 21, the Kitty Color Protocol.
+pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command {
+ assert(parser.state == .@"21");
+
+ const alloc = parser.alloc orelse {
+ parser.state = .invalid;
+ return null;
+ };
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ parser.command = .{
+ .kitty_color_protocol = .{
+ .list = .empty,
+ .terminator = .init(terminator_ch),
+ },
+ };
+ const list = &parser.command.kitty_color_protocol.list;
+ const data = cap.trailing();
+ var kv_it = std.mem.splitScalar(u8, data, ';');
+ while (kv_it.next()) |kv| {
+ if (list.items.len >= @as(usize, kitty_color.Kind.max) * 2) {
+ log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{});
+ parser.state = .invalid;
+ return null;
+ }
+ var it = std.mem.splitScalar(u8, kv, '=');
+ const k = it.next() orelse continue;
+ if (k.len == 0) {
+ log.warn("zero length key in kitty color protocol", .{});
+ continue;
+ }
+ const key = kitty_color.Kind.parse(k) orelse {
+ log.warn("unknown key in kitty color protocol: {s}", .{k});
+ continue;
+ };
+ const value = std.mem.trim(u8, it.rest(), " ");
+ if (value.len == 0) {
+ list.append(alloc, .{ .reset = key }) catch |err| {
+ log.warn("unable to append kitty color protocol option: {}", .{err});
+ continue;
+ };
+ } else if (std.mem.eql(u8, "?", value)) {
+ list.append(alloc, .{ .query = key }) catch |err| {
+ log.warn("unable to append kitty color protocol option: {}", .{err});
+ continue;
+ };
+ } else {
+ list.append(alloc, .{
+ .set = .{
+ .key = key,
+ .color = RGB.parse(value) catch |err| switch (err) {
+ error.InvalidFormat => {
+ log.warn("invalid color format in kitty color protocol: {s}", .{value});
+ continue;
+ },
+ },
+ },
+ }) catch |err| {
+ log.warn("unable to append kitty color protocol option: {}", .{err});
+ continue;
+ };
+ }
+ }
+ return &parser.command;
+}
+
+test "OSC 21: kitty color protocol" {
+ const testing = std.testing;
+ const Kind = kitty_color.Kind;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_color_protocol);
+ try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len);
+ {
+ const item = cmd.kitty_color_protocol.list.items[0];
+ try testing.expect(item == .query);
+ try testing.expectEqual(Kind{ .special = .foreground }, item.query);
+ }
+ {
+ const item = cmd.kitty_color_protocol.list.items[1];
+ try testing.expect(item == .set);
+ try testing.expectEqual(Kind{ .special = .background }, item.set.key);
+ try testing.expectEqual(@as(u8, 0xf0), item.set.color.r);
+ try testing.expectEqual(@as(u8, 0xf8), item.set.color.g);
+ try testing.expectEqual(@as(u8, 0xff), item.set.color.b);
+ }
+ {
+ const item = cmd.kitty_color_protocol.list.items[2];
+ try testing.expect(item == .set);
+ try testing.expectEqual(Kind{ .special = .cursor }, item.set.key);
+ try testing.expectEqual(@as(u8, 0xf0), item.set.color.r);
+ try testing.expectEqual(@as(u8, 0xf8), item.set.color.g);
+ try testing.expectEqual(@as(u8, 0xff), item.set.color.b);
+ }
+ {
+ const item = cmd.kitty_color_protocol.list.items[3];
+ try testing.expect(item == .reset);
+ try testing.expectEqual(Kind{ .special = .cursor_text }, item.reset);
+ }
+ {
+ const item = cmd.kitty_color_protocol.list.items[4];
+ try testing.expect(item == .reset);
+ try testing.expectEqual(Kind{ .special = .visual_bell }, item.reset);
+ }
+ {
+ const item = cmd.kitty_color_protocol.list.items[5];
+ try testing.expect(item == .query);
+ try testing.expectEqual(Kind{ .special = .selection_background }, item.query);
+ }
+ {
+ const item = cmd.kitty_color_protocol.list.items[6];
+ try testing.expect(item == .set);
+ try testing.expectEqual(Kind{ .special = .selection_background }, item.set.key);
+ try testing.expectEqual(@as(u8, 0xaa), item.set.color.r);
+ try testing.expectEqual(@as(u8, 0xbb), item.set.color.g);
+ try testing.expectEqual(@as(u8, 0xcc), item.set.color.b);
+ }
+ {
+ const item = cmd.kitty_color_protocol.list.items[7];
+ try testing.expect(item == .query);
+ try testing.expectEqual(Kind{ .palette = 2 }, item.query);
+ }
+ {
+ const item = cmd.kitty_color_protocol.list.items[8];
+ try testing.expect(item == .set);
+ try testing.expectEqual(Kind{ .palette = 3 }, item.set.key);
+ try testing.expectEqual(@as(u8, 0xff), item.set.color.r);
+ try testing.expectEqual(@as(u8, 0xff), item.set.color.g);
+ try testing.expectEqual(@as(u8, 0xff), item.set.color.b);
+ }
+}
+
+test "OSC 21: kitty color protocol without allocator" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ defer p.deinit();
+
+ const input = "21;foreground=?";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC 21: kitty color protocol double reset" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_color_protocol);
+
+ p.reset();
+ p.reset();
+}
+
+test "OSC 21: kitty color protocol reset after invalid" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_color_protocol);
+
+ p.reset();
+
+ try testing.expectEqual(Parser.State.start, p.state);
+ p.next('X');
+ try testing.expectEqual(Parser.State.invalid, p.state);
+
+ p.reset();
+}
+
+test "OSC 21: kitty color protocol no key" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "21;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_color_protocol);
+ try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len);
+}
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
new file mode 100644
index 00000000..94f27100
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/kitty_text_sizing.zig
@@ -0,0 +1,255 @@
+//! Kitty's text sizing protocol (OSC 66)
+//! Specification: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/
+
+const std = @import("std");
+
+const assert = @import("../../../quirks.zig").inlineAssert;
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+const encoding = @import("../encoding.zig");
+const lib = @import("../../lib.zig");
+
+const log = std.log.scoped(.kitty_text_sizing);
+
+pub const max_payload_length = 4096;
+
+pub const VAlign = lib.Enum(lib.target, &.{
+ "top",
+ "bottom",
+ "center",
+});
+
+pub const HAlign = lib.Enum(lib.target, &.{
+ "left",
+ "right",
+ "center",
+});
+
+pub const OSC = struct {
+ scale: u3 = 1, // 1 - 7
+ width: u3 = 0, // 0 - 7 (0 means default)
+ numerator: u4 = 0,
+ denominator: u4 = 0,
+ valign: VAlign = .top,
+ halign: HAlign = .left,
+ text: [:0]const u8,
+
+ /// We don't currently support encoding this to C in any way.
+ pub const C = void;
+
+ pub fn cval(_: OSC) C {
+ return {};
+ }
+
+ fn update(self: *OSC, key: u8, value: []const u8) error{
+ UnknownKey,
+ InvalidValue,
+ }!void {
+ // All values are numeric, so we can do a small hack here
+ const v = std.fmt.parseInt(
+ u4,
+ value,
+ 10,
+ ) catch return error.InvalidValue;
+
+ switch (key) {
+ 's' => {
+ if (v == 0) return error.InvalidValue;
+ self.scale = std.math.cast(u3, v) orelse return error.InvalidValue;
+ },
+ 'w' => self.width = std.math.cast(u3, v) orelse return error.InvalidValue,
+ 'n' => self.numerator = v,
+ 'd' => self.denominator = v,
+ 'v' => self.valign = std.enums.fromInt(VAlign, v) orelse return error.InvalidValue,
+ 'h' => self.halign = std.enums.fromInt(HAlign, v) orelse return error.InvalidValue,
+ else => return error.UnknownKey,
+ }
+ }
+};
+
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ assert(parser.state == .@"66");
+
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+
+ // Write a NUL byte to ensure that `text` is NUL-terminated
+ cap.writer.writeByte(0) catch {
+ 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 payload = data[payload_start + 1 .. data.len - 1 :0];
+
+ // Payload has to be a URL-safe UTF-8 string,
+ // and be under the size limit.
+ if (payload.len > max_payload_length) {
+ log.warn("payload is too long", .{});
+ parser.state = .invalid;
+ return null;
+ }
+ if (!encoding.isSafeUtf8(payload)) {
+ log.warn("payload is not escape code safe UTF-8", .{});
+ parser.state = .invalid;
+ return null;
+ }
+
+ parser.command = .{
+ .kitty_text_sizing = .{ .text = payload },
+ };
+ const cmd = &parser.command.kitty_text_sizing;
+
+ // Parse any arguments if given
+ if (payload_start > 0) {
+ var kv_it = std.mem.splitScalar(
+ u8,
+ data[0..payload_start],
+ ':',
+ );
+
+ while (kv_it.next()) |kv| {
+ var it = std.mem.splitScalar(u8, kv, '=');
+ const k = it.next() orelse {
+ log.warn("missing key", .{});
+ continue;
+ };
+ if (k.len != 1) {
+ log.warn("key must be a single character", .{});
+ continue;
+ }
+
+ const value = it.next() orelse {
+ log.warn("missing value", .{});
+ continue;
+ };
+
+ cmd.update(k[0], value) catch |err| {
+ switch (err) {
+ error.UnknownKey => log.warn("unknown key: '{c}'", .{k[0]}),
+ error.InvalidValue => log.warn("invalid value for key '{c}': {}", .{ k[0], err }),
+ }
+ continue;
+ };
+ }
+ }
+
+ return &parser.command;
+}
+
+test "OSC 66: empty parameters" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "66;;bobr";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_text_sizing);
+ try testing.expectEqual(1, cmd.kitty_text_sizing.scale);
+ try testing.expectEqualStrings("bobr", cmd.kitty_text_sizing.text);
+}
+
+test "OSC 66: single parameter" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "66;s=2;kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_text_sizing);
+ try testing.expectEqual(2, cmd.kitty_text_sizing.scale);
+ try testing.expectEqualStrings("kurwa", cmd.kitty_text_sizing.text);
+}
+
+test "OSC 66: multiple parameters" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "66;s=2:w=7:n=13:d=15:v=1:h=2;long";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_text_sizing);
+ try testing.expectEqual(2, cmd.kitty_text_sizing.scale);
+ try testing.expectEqual(7, cmd.kitty_text_sizing.width);
+ try testing.expectEqual(13, cmd.kitty_text_sizing.numerator);
+ try testing.expectEqual(15, cmd.kitty_text_sizing.denominator);
+ try testing.expectEqual(.bottom, cmd.kitty_text_sizing.valign);
+ try testing.expectEqual(.center, cmd.kitty_text_sizing.halign);
+ try testing.expectEqualStrings("long", cmd.kitty_text_sizing.text);
+}
+
+test "OSC 66: scale is zero" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "66;s=0;nope";
+ for (input) |ch| p.next(ch);
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .kitty_text_sizing);
+ try testing.expectEqual(1, cmd.kitty_text_sizing.scale);
+}
+
+test "OSC 66: invalid parameters" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ for ("66;w=8:v=3:n=16;") |ch| p.next(ch);
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .kitty_text_sizing);
+ try testing.expectEqual(0, cmd.kitty_text_sizing.width);
+ try testing.expect(cmd.kitty_text_sizing.valign == .top);
+ try testing.expectEqual(0, cmd.kitty_text_sizing.numerator);
+}
+
+test "OSC 66: UTF-8" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "66;;👻魑魅魍魉ゴースッティ";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .kitty_text_sizing);
+ try testing.expectEqualStrings("👻魑魅魍魉ゴースッティ", cmd.kitty_text_sizing.text);
+}
+
+test "OSC 66: unsafe UTF-8" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "66;;\n";
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
+
+test "OSC 66: overlong UTF-8" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "66;;" ++ "bobr" ** 1025;
+ for (input) |ch| p.next(ch);
+
+ try testing.expect(p.end('\x1b') == null);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/mouse_shape.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/mouse_shape.zig
new file mode 100644
index 00000000..4b96c038
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/mouse_shape.zig
@@ -0,0 +1,39 @@
+const std = @import("std");
+
+const assert = @import("../../../quirks.zig").inlineAssert;
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+// Parse OSC 22
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ assert(parser.state == .@"22");
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ cap.writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ parser.command = .{
+ .mouse_shape = .{
+ .value = data[0 .. data.len - 1 :0],
+ },
+ };
+ return &parser.command;
+}
+
+test "OSC 22: pointer cursor" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "22;pointer";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .mouse_shape);
+ try testing.expectEqualStrings("pointer", cmd.mouse_shape.value);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/osc9.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/osc9.zig
new file mode 100644
index 00000000..4b8dda9b
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/osc9.zig
@@ -0,0 +1,1141 @@
+const std = @import("std");
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+/// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension.
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ const writer = cap.writer;
+
+ // Check first to see if this is a ConEmu OSC
+ // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
+ conemu: {
+ var data = cap.trailing();
+ if (data.len == 0) break :conemu;
+ switch (data[0]) {
+ // Check for OSC 9;1 9;10 9;11 9;12
+ '1' => {
+ if (data.len < 2) break :conemu;
+ switch (data[1]) {
+ // OSC 9;1 sleep
+ ';' => {
+ parser.command = .{
+ .conemu_sleep = .{
+ .duration_ms = if (std.fmt.parseUnsigned(u16, data[2..], 10)) |num| @min(num, 10_000) else |_| 100,
+ },
+ };
+ return &parser.command;
+ },
+ // OSC 9;10 xterm keyboard and output emulation
+ '0' => {
+ if (data.len == 2) {
+ parser.command = .{
+ .conemu_xterm_emulation = .{
+ .keyboard = true,
+ .output = true,
+ },
+ };
+ return &parser.command;
+ }
+ if (data.len < 4) break :conemu;
+ if (data[2] != ';') break :conemu;
+ switch (data[3]) {
+ '0' => {
+ parser.command = .{
+ .conemu_xterm_emulation = .{
+ .keyboard = false,
+ .output = false,
+ },
+ };
+ return &parser.command;
+ },
+ '1' => {
+ parser.command = .{
+ .conemu_xterm_emulation = .{
+ .keyboard = true,
+ .output = true,
+ },
+ };
+ return &parser.command;
+ },
+ '2' => {
+ parser.command = .{
+ .conemu_xterm_emulation = .{
+ .keyboard = null,
+ .output = false,
+ },
+ };
+ return &parser.command;
+ },
+ '3' => {
+ parser.command = .{
+ .conemu_xterm_emulation = .{
+ .keyboard = null,
+ .output = true,
+ },
+ };
+ return &parser.command;
+ },
+ else => break :conemu,
+ }
+ },
+ // OSC 9;11 comment
+ '1' => {
+ if (data.len < 3) break :conemu;
+ if (data[2] != ';') break :conemu;
+ writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ data = cap.trailing();
+ parser.command = .{
+ .conemu_comment = data[3 .. data.len - 1 :0],
+ };
+ return &parser.command;
+ },
+ // OSC 9;12 mark prompt start
+ '2' => {
+ parser.command = .{ .semantic_prompt = .init(.fresh_line_new_prompt) };
+ return &parser.command;
+ },
+ else => break :conemu,
+ }
+ },
+ // OSC 9;2 show message box
+ '2' => {
+ if (data.len < 2) break :conemu;
+ if (data[1] != ';') break :conemu;
+ writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ data = cap.trailing();
+ parser.command = .{
+ .conemu_show_message_box = data[2 .. data.len - 1 :0],
+ };
+ return &parser.command;
+ },
+ // OSC 9;3 change tab title
+ '3' => {
+ if (data.len < 2) break :conemu;
+ if (data[1] != ';') break :conemu;
+ if (data.len == 2) {
+ parser.command = .{
+ .conemu_change_tab_title = .reset,
+ };
+ return &parser.command;
+ }
+ writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ data = cap.trailing();
+ parser.command = .{
+ .conemu_change_tab_title = .{
+ .value = data[2 .. data.len - 1 :0],
+ },
+ };
+ return &parser.command;
+ },
+ // OSC 9;4 progress report
+ '4' => {
+ if (data.len < 2) break :conemu;
+ if (data[1] != ';') break :conemu;
+ if (data.len < 3) break :conemu;
+ switch (data[2]) {
+ '0' => {
+ parser.command = .{
+ .conemu_progress_report = .{
+ .state = .remove,
+ },
+ };
+ },
+ '1' => {
+ parser.command = .{
+ .conemu_progress_report = .{
+ .state = .set,
+ .progress = 0,
+ },
+ };
+ },
+ '2' => {
+ parser.command = .{
+ .conemu_progress_report = .{
+ .state = .@"error",
+ },
+ };
+ },
+ '3' => {
+ parser.command = .{
+ .conemu_progress_report = .{
+ .state = .indeterminate,
+ },
+ };
+ },
+ '4' => {
+ parser.command = .{
+ .conemu_progress_report = .{
+ .state = .pause,
+ },
+ };
+ },
+ else => break :conemu,
+ }
+ switch (parser.command.conemu_progress_report.state) {
+ .remove, .indeterminate => {},
+ .set, .@"error", .pause => progress: {
+ if (data.len < 4) break :progress;
+ if (data[3] != ';') break :progress;
+ // parse the progress value
+ parser.command.conemu_progress_report.progress = value: {
+ break :value @intCast(std.math.clamp(
+ std.fmt.parseUnsigned(usize, data[4..], 10) catch break :value null,
+ 0,
+ 100,
+ ));
+ };
+ },
+ }
+ return &parser.command;
+ },
+ // OSC 9;5 wait for input
+ '5' => {
+ parser.command = .conemu_wait_input;
+ return &parser.command;
+ },
+ // OSC 9;6 guimacro
+ '6' => {
+ if (data.len < 2) break :conemu;
+ if (data[1] != ';') break :conemu;
+ writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ data = cap.trailing();
+ parser.command = .{
+ .conemu_guimacro = data[2 .. data.len - 1 :0],
+ };
+ return &parser.command;
+ },
+ // OSC 9;7 run process
+ '7' => {
+ if (data.len < 2) break :conemu;
+ if (data[1] != ';') break :conemu;
+ writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ data = cap.trailing();
+ parser.command = .{
+ .conemu_run_process = data[2 .. data.len - 1 :0],
+ };
+ return &parser.command;
+ },
+ // OSC 9;8 output environment variable
+ '8' => {
+ if (data.len < 2) break :conemu;
+ if (data[1] != ';') break :conemu;
+ writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ data = cap.trailing();
+ parser.command = .{
+ .conemu_output_environment_variable = data[2 .. data.len - 1 :0],
+ };
+ return &parser.command;
+ },
+ // OSC 9;9 current working directory
+ '9' => {
+ if (data.len < 2) break :conemu;
+ if (data[1] != ';') break :conemu;
+ writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ data = cap.trailing();
+ parser.command = .{
+ .report_pwd = .{
+ .value = data[2 .. data.len - 1 :0],
+ },
+ };
+ return &parser.command;
+ },
+ else => break :conemu,
+ }
+ }
+
+ // If it's not a ConEmu OSC, it's an iTerm2 notification
+
+ writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ parser.command = .{
+ .show_desktop_notification = .{
+ .title = "",
+ .body = data[0 .. data.len - 1 :0],
+ },
+ };
+ return &parser.command;
+}
+
+test "OSC 9: show desktop notification" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;Hello world";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("", cmd.show_desktop_notification.title);
+ try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9: show single character desktop notification" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;H";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("", cmd.show_desktop_notification.title);
+ try testing.expectEqualStrings("H", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;1: ConEmu sleep" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;1;420";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .conemu_sleep);
+ try testing.expectEqual(420, cmd.conemu_sleep.duration_ms);
+}
+
+test "OSC 9;1: ConEmu sleep with no value default to 100ms" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;1;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .conemu_sleep);
+ try testing.expectEqual(100, cmd.conemu_sleep.duration_ms);
+}
+
+test "OSC 9;1: conemu sleep cannot exceed 10000ms" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;1;12345";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .conemu_sleep);
+ try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms);
+}
+
+test "OSC 9;1: conemu sleep invalid input" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;1;foo";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .conemu_sleep);
+ try testing.expectEqual(100, cmd.conemu_sleep.duration_ms);
+}
+
+test "OSC 9;1: conemu sleep -> desktop notification 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;1";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("1", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;1: conemu sleep -> desktop notification 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;1a";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;2: ConEmu message box" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;2;hello world";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_show_message_box);
+ try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box);
+}
+
+test "OSC 9;2: ConEmu message box invalid input" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;2";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("2", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;2: ConEmu message box empty message" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;2;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_show_message_box);
+ try testing.expectEqualStrings("", cmd.conemu_show_message_box);
+}
+
+test "OSC 9;2: ConEmu message box spaces only message" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;2; ";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_show_message_box);
+ try testing.expectEqualStrings(" ", cmd.conemu_show_message_box);
+}
+
+test "OSC 9;2: message box -> desktop notification 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;2";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("2", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;2: message box -> desktop notification 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;2a";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;3: ConEmu change tab title" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;3;foo bar";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_change_tab_title);
+ try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value);
+}
+
+test "OSC 9;3: ConEmu change tab title reset" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;3;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ const expected_command: Command = .{ .conemu_change_tab_title = .reset };
+ try testing.expectEqual(expected_command, cmd);
+}
+
+test "OSC 9;3: ConEmu change tab title spaces only" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;3; ";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .conemu_change_tab_title);
+ try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value);
+}
+
+test "OSC 9;3: change tab title -> desktop notification 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;3";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("3", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;3: message box -> desktop notification 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;3a";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;4: ConEmu progress set" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;1;100";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .set);
+ try testing.expect(cmd.conemu_progress_report.progress == 100);
+}
+
+test "OSC 9;4: ConEmu progress set overflow" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;1;900";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .set);
+ try testing.expectEqual(100, cmd.conemu_progress_report.progress);
+}
+
+test "OSC 9;4: ConEmu progress set single digit" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;1;9";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .set);
+ try testing.expect(cmd.conemu_progress_report.progress == 9);
+}
+
+test "OSC 9;4: ConEmu progress set double digit" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;1;94";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .set);
+ try testing.expectEqual(94, cmd.conemu_progress_report.progress);
+}
+
+test "OSC 9;4: ConEmu progress set extra semicolon ignored" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;1;100";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .set);
+ try testing.expectEqual(100, cmd.conemu_progress_report.progress);
+}
+
+test "OSC 9;4: ConEmu progress remove with no progress" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;0;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .remove);
+ try testing.expect(cmd.conemu_progress_report.progress == null);
+}
+
+test "OSC 9;4: ConEmu progress remove with double semicolon" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;0;;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .remove);
+ try testing.expect(cmd.conemu_progress_report.progress == null);
+}
+
+test "OSC 9;4: ConEmu progress remove ignores progress" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;0;100";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .remove);
+ try testing.expect(cmd.conemu_progress_report.progress == null);
+}
+
+test "OSC 9;4: ConEmu progress remove extra semicolon" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;0;100;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .remove);
+}
+
+test "OSC 9;4: ConEmu progress error" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;2";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .@"error");
+ try testing.expect(cmd.conemu_progress_report.progress == null);
+}
+
+test "OSC 9;4: ConEmu progress error with progress" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;2;100";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .@"error");
+ try testing.expect(cmd.conemu_progress_report.progress == 100);
+}
+
+test "OSC 9;4: progress pause" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;4";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .pause);
+ try testing.expect(cmd.conemu_progress_report.progress == null);
+}
+
+test "OSC 9;4: ConEmu progress pause with progress" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;4;100";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_progress_report);
+ try testing.expect(cmd.conemu_progress_report.state == .pause);
+ try testing.expect(cmd.conemu_progress_report.progress == 100);
+}
+
+test "OSC 9;4: progress -> desktop notification 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("4", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;4: progress -> desktop notification 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;4: progress -> desktop notification 3" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;5";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;4: progress -> desktop notification 4" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;4;5a";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body);
+}
+
+test "OSC 9;5: ConEmu wait input" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;5";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_wait_input);
+}
+
+test "OSC 9;5: ConEmu wait ignores trailing characters" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "9;5;foo";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_wait_input);
+}
+
+test "OSC 9;6: ConEmu guimacro 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;6;a";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_guimacro);
+ try testing.expectEqualStrings("a", cmd.conemu_guimacro);
+}
+
+test "OSC: 9;6: ConEmu guimacro 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;6;ab";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_guimacro);
+ try testing.expectEqualStrings("ab", cmd.conemu_guimacro);
+}
+
+test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;6";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("6", cmd.show_desktop_notification.body);
+}
+
+test "OSC: 9;7: ConEmu run process 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;7;ab";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_run_process);
+ try testing.expectEqualStrings("ab", cmd.conemu_run_process);
+}
+
+test "OSC: 9;7: ConEmu run process 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;7;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_run_process);
+ try testing.expectEqualStrings("", cmd.conemu_run_process);
+}
+
+test "OSC: 9;7: ConEmu run process incomplete -> desktop notification" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;7";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("7", cmd.show_desktop_notification.body);
+}
+
+test "OSC: 9;8: ConEmu output environment variable 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;8;ab";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_output_environment_variable);
+ try testing.expectEqualStrings("ab", cmd.conemu_output_environment_variable);
+}
+
+test "OSC: 9;8: ConEmu output environment variable 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;8;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_output_environment_variable);
+ try testing.expectEqualStrings("", cmd.conemu_output_environment_variable);
+}
+
+test "OSC: 9;8: ConEmu output environment variable incomplete -> desktop notification" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;8";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("8", cmd.show_desktop_notification.body);
+}
+
+test "OSC: 9;9: ConEmu set current working directory" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;9;ab";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .report_pwd);
+ try testing.expectEqualStrings("ab", cmd.report_pwd.value);
+}
+
+test "OSC: 9;9: ConEmu set current working directory incomplete -> desktop notification" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;9";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("9", cmd.show_desktop_notification.body);
+}
+
+test "OSC: 9;10: ConEmu xterm keyboard and output emulation 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;10";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_xterm_emulation);
+ try testing.expect(cmd.conemu_xterm_emulation.keyboard != null);
+ try testing.expect(cmd.conemu_xterm_emulation.keyboard.? == true);
+ try testing.expect(cmd.conemu_xterm_emulation.output != null);
+ try testing.expect(cmd.conemu_xterm_emulation.output.? == true);
+}
+
+test "OSC: 9;10: ConEmu xterm keyboard and output emulation 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;10;0";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_xterm_emulation);
+ try testing.expect(cmd.conemu_xterm_emulation.keyboard != null);
+ try testing.expect(cmd.conemu_xterm_emulation.keyboard.? == false);
+ try testing.expect(cmd.conemu_xterm_emulation.output != null);
+ try testing.expect(cmd.conemu_xterm_emulation.output.? == false);
+}
+
+test "OSC: 9;10: ConEmu xterm keyboard and output emulation 3" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;10;1";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_xterm_emulation);
+ try testing.expect(cmd.conemu_xterm_emulation.keyboard != null);
+ try testing.expect(cmd.conemu_xterm_emulation.keyboard.? == true);
+ try testing.expect(cmd.conemu_xterm_emulation.output != null);
+ try testing.expect(cmd.conemu_xterm_emulation.output.? == true);
+}
+
+test "OSC: 9;10: ConEmu xterm keyboard and output emulation 4" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;10;2";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_xterm_emulation);
+ try testing.expect(cmd.conemu_xterm_emulation.keyboard == null);
+ try testing.expect(cmd.conemu_xterm_emulation.output != null);
+ try testing.expect(cmd.conemu_xterm_emulation.output.? == false);
+}
+
+test "OSC: 9;10: ConEmu xterm keyboard and output emulation 5" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;10;3";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_xterm_emulation);
+ try testing.expect(cmd.conemu_xterm_emulation.keyboard == null);
+ try testing.expect(cmd.conemu_xterm_emulation.output != null);
+ try testing.expect(cmd.conemu_xterm_emulation.output.? == true);
+}
+
+test "OSC: 9;10: ConEmu xterm keyboard and output emulation 6" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;10;4";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("10;4", cmd.show_desktop_notification.body);
+}
+
+test "OSC: 9;10: ConEmu xterm keyboard and output emulation 7" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;10;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("10;", cmd.show_desktop_notification.body);
+}
+
+test "OSC: 9;10: ConEmu xterm keyboard and output emulation 8" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;10;abc";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("10;abc", cmd.show_desktop_notification.body);
+}
+
+test "OSC: 9;11: ConEmu comment" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;11;ab";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .conemu_comment);
+ try testing.expectEqualStrings("ab", cmd.conemu_comment);
+}
+
+test "OSC: 9;11: ConEmu comment incomplete -> desktop notification" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;11";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings("11", cmd.show_desktop_notification.body);
+}
+
+test "OSC: 9;12: ConEmu mark prompt start 1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;12";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .semantic_prompt);
+}
+
+test "OSC: 9;12: ConEmu mark prompt start 2" {
+ const testing = std.testing;
+
+ var p: Parser = .init(testing.allocator);
+ defer p.deinit();
+
+ const input = "9;12;abc";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .semantic_prompt);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/report_pwd.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/report_pwd.zig
new file mode 100644
index 00000000..f99bc894
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/report_pwd.zig
@@ -0,0 +1,48 @@
+const std = @import("std");
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+/// Parse OSC 7
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ cap.writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ parser.command = .{
+ .report_pwd = .{
+ .value = data[0 .. data.len - 1 :0],
+ },
+ };
+ return &parser.command;
+}
+
+test "OSC 7: report pwd" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "7;file:///tmp/example";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .report_pwd);
+ try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value);
+}
+
+test "OSC 7: report pwd empty" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "7;";
+ for (input) |ch| p.next(ch);
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .report_pwd);
+ try testing.expectEqualStrings("", cmd.report_pwd.value);
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/rxvt_extension.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/rxvt_extension.zig
new file mode 100644
index 00000000..f5b965c6
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/rxvt_extension.zig
@@ -0,0 +1,59 @@
+const std = @import("std");
+
+const Parser = @import("../../osc.zig").Parser;
+const Command = @import("../../osc.zig").Command;
+
+const log = std.log.scoped(.osc_rxvt_extension);
+
+/// Parse OSC 777
+pub fn parse(parser: *Parser, _: ?u8) ?*Command {
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ // ensure that we are sentinel terminated
+ cap.writer.writeByte(0) catch {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ const k = std.mem.indexOfScalar(u8, data, ';') orelse {
+ parser.state = .invalid;
+ return null;
+ };
+ const ext = data[0..k];
+ if (!std.mem.eql(u8, ext, "notify")) {
+ log.warn("unknown rxvt extension: {s}", .{ext});
+ parser.state = .invalid;
+ return null;
+ }
+ const t = std.mem.indexOfScalarPos(u8, data, k + 1, ';') orelse {
+ log.warn("rxvt notify extension is missing the title", .{});
+ parser.state = .invalid;
+ return null;
+ };
+ data[t] = 0;
+ const title = data[k + 1 .. t :0];
+ const body = data[t + 1 .. data.len - 1 :0];
+ parser.command = .{
+ .show_desktop_notification = .{
+ .title = title,
+ .body = body,
+ },
+ };
+ return &parser.command;
+}
+
+test "OSC: OSC 777 show desktop notification with title" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "777;notify;Title;Body";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end('\x1b').?.*;
+ try testing.expect(cmd == .show_desktop_notification);
+ try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title");
+ try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body");
+}
diff --git a/vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig b/vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig
new file mode 100644
index 00000000..25152d7c
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/osc/parsers/semantic_prompt.zig
@@ -0,0 +1,1264 @@
+//! https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
+const std = @import("std");
+
+const Parser = @import("../../osc.zig").Parser;
+const OSCCommand = @import("../../osc.zig").Command;
+const string_encoding = @import("../../../os/string_encoding.zig");
+
+const log = std.log.scoped(.osc_semantic_prompt);
+
+/// A single semantic prompt command.
+///
+/// Technically according to the spec, not all commands have options
+/// but it is easier to be "liberal in what we accept" here since
+/// all except one do and the spec does also say to ignore unknown
+/// options. So, I think this is a fair interpretation.
+pub const Command = struct {
+ pub const C = void;
+
+ action: Action,
+ options_unvalidated: []const u8,
+
+ pub const Action = enum {
+ fresh_line, // 'L'
+ fresh_line_new_prompt, // 'A'
+ new_command, // 'N'
+ prompt_start, // 'P'
+ end_prompt_start_input, // 'B'
+ end_prompt_start_input_terminate_eol, // 'I'
+ end_input_start_output, // 'C'
+ end_command, // 'D'
+ };
+
+ pub fn init(action: Action) Command {
+ return .{
+ .action = action,
+ .options_unvalidated = "",
+ };
+ }
+
+ /// Read an option for this command. Returns null if unset or invalid.
+ pub fn readOption(
+ self: Command,
+ comptime option: Option,
+ ) ?option.Type() {
+ return option.read(self.options_unvalidated);
+ }
+
+ /// Write the decoded command line (if any) to the writer. If an error
+ /// occurs garbage may have been written to the writer.
+ pub fn writeCommandLine(self: Command, writer: *std.Io.Writer) (std.Io.Writer.Error || error{DecodeError})!void {
+ if (self.readOption(.cmdline)) |command_line| {
+ try string_encoding.printfQDecode(writer, command_line);
+ return;
+ }
+ if (self.readOption(.cmdline_url)) |command_line| {
+ try string_encoding.urlPercentDecode(writer, command_line);
+ return;
+ }
+ return;
+ }
+};
+
+pub const Option = enum {
+ aid,
+ cl,
+ prompt_kind,
+ err,
+ cmdline,
+ cmdline_url,
+
+ // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
+ // Kitty supports a "redraw" option for prompt_start. This is extended
+ // by Ghostty with the "last" option. See Redraw the type for more details.
+ redraw,
+
+ // Use a special key instead of arrow keys to move the cursor on
+ // mouse click. Useful if arrow keys have side-effets like triggering
+ // auto-complete. The shell integration script should bind the special
+ // key as needed.
+ // See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
+ special_key,
+
+ // If true, the shell is capable of handling mouse click events.
+ // Ghostty will then send a click event to the shell when the user
+ // clicks somewhere in the prompt. The shell can then move the cursor
+ // to that position or perform some other appropriate action. If false,
+ // Ghostty may generate a number of fake key events to move the cursor
+ // which is not very robust.
+ // See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
+ click_events,
+
+ // Not technically an option that can be set with k=v and only
+ // present currently with command 'D' but its easier to just
+ // parse it into our options.
+ exit_code,
+
+ pub fn Type(comptime self: Option) type {
+ return switch (self) {
+ .aid => []const u8,
+ .cl => Click,
+ .prompt_kind => PromptKind,
+ .err => []const u8,
+ .redraw => Redraw,
+ .special_key => bool,
+ .click_events => bool,
+ .cmdline => []const u8,
+ .cmdline_url => []const u8,
+ .exit_code => i32,
+ };
+ }
+
+ fn key(comptime self: Option) []const u8 {
+ return switch (self) {
+ .aid => "aid",
+ .cl => "cl",
+ .prompt_kind => "k",
+ .err => "err",
+ .redraw => "redraw",
+ .special_key => "special_key",
+ .click_events => "click_events",
+ .cmdline => "cmdline",
+ .cmdline_url => "cmdline_url",
+
+ // special case, handled before ever calling key
+ .exit_code => unreachable,
+ };
+ }
+
+ /// Read the option value from the raw options string.
+ ///
+ /// The raw options string is the raw unparsed data after the
+ /// OSC 133 command. e.g. for `133;A;aid=14;cl=line`, the
+ /// raw options string would be `aid=14;cl=line`.
+ ///
+ /// Any errors in the raw string will return null since the OSC133
+ /// specification says to ignore unknown or malformed options.
+ pub fn read(
+ comptime self: Option,
+ raw: []const u8,
+ ) ?self.Type() {
+ var remaining = raw;
+ while (remaining.len > 0) {
+ // Length of the next value is up to the `;` or the
+ // end of the string.
+ const len = std.mem.indexOfScalar(
+ u8,
+ remaining,
+ ';',
+ ) orelse remaining.len;
+
+ // Grab our full value and move our cursor past the `;`
+ const full = remaining[0..len];
+
+ // If we're looking for exit_code we special case it.
+ // as the first value.
+ if (comptime self == .exit_code) {
+ return std.fmt.parseInt(
+ i32,
+ full,
+ 10,
+ ) catch null;
+ }
+
+ // Parse our key=value and verify our key matches our
+ // expectation.
+ const value = value: {
+ if (std.mem.indexOfScalar(
+ u8,
+ full,
+ '=',
+ )) |eql_idx| {
+ if (std.mem.eql(
+ u8,
+ full[0..eql_idx],
+ self.key(),
+ )) {
+ break :value full[eql_idx + 1 ..];
+ }
+ }
+
+ // No match!
+ if (len < remaining.len) {
+ remaining = remaining[len + 1 ..];
+ continue;
+ }
+
+ break;
+ };
+
+ return switch (self) {
+ .aid => value,
+ .cl => .init(value),
+ .prompt_kind => if (value.len == 1) PromptKind.init(value[0]) else null,
+ .err => value,
+ .redraw => if (std.mem.eql(u8, value, "0"))
+ .false
+ else if (std.mem.eql(u8, value, "1"))
+ .true
+ else if (std.mem.eql(u8, value, "last"))
+ .last
+ else
+ null,
+ .special_key, .click_events => if (value.len == 1) switch (value[0]) {
+ '0' => false,
+ '1' => true,
+ else => null,
+ } else null,
+ .cmdline => value,
+ .cmdline_url => value,
+ // Handled above
+ .exit_code => unreachable,
+ };
+ }
+
+ // Not found
+ return null;
+ }
+};
+
+/// 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;
+ }
+};
+
+pub const PromptKind = enum {
+ initial,
+ right,
+ continuation,
+ secondary,
+
+ pub fn init(c: u8) ?PromptKind {
+ return switch (c) {
+ 'i' => .initial,
+ 'r' => .right,
+ 'c' => .continuation,
+ 's' => .secondary,
+ else => null,
+ };
+ }
+};
+
+/// The values for the `redraw` extension to OSC133. This was
+/// started by Kitty[1] and extended by Ghostty (the "last" option).
+///
+/// [1]: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
+pub const Redraw = enum(u2) {
+ /// The shell supports redrawing the full prompt and all continuations.
+ /// This is the default value, it does not need to be explicitly set
+ /// unless it is to reset a prior other value.
+ true,
+
+ /// The shell does NOT support redrawing. In this case, Ghostty will NOT
+ /// clear any prompt lines on resize.
+ false,
+
+ /// The shell supports redrawing only the LAST line of the prompt.
+ /// Ghostty will only clear the last line of the prompt on resize.
+ ///
+ /// This is specifically introduced because Bash only redraws the last
+ /// line. It is literally the only shell that does this and it does this
+ /// because its bad and they should feel bad. Don't be like Bash.
+ last,
+};
+
+/// Parse OSC 133, semantic prompts
+pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand {
+ const cap = if (parser.capture) |*c| c else {
+ parser.state = .invalid;
+ return null;
+ };
+ const data = cap.trailing();
+ if (data.len == 0) {
+ parser.state = .invalid;
+ return null;
+ }
+
+ // All valid cases terminate within this block. Any fallthroughs
+ // are invalid. This makes some of our parse logic a little less
+ // repetitive.
+ valid: {
+ switch (data[0]) {
+ 'A' => fresh_line: {
+ parser.command = .{ .semantic_prompt = .init(.fresh_line_new_prompt) };
+ if (data.len == 1) break :fresh_line;
+ if (data[1] != ';') break :valid;
+ parser.command.semantic_prompt.options_unvalidated = data[2..];
+ },
+
+ 'B' => end_prompt: {
+ parser.command = .{ .semantic_prompt = .init(.end_prompt_start_input) };
+ if (data.len == 1) break :end_prompt;
+ if (data[1] != ';') break :valid;
+ parser.command.semantic_prompt.options_unvalidated = data[2..];
+ },
+
+ 'I' => end_prompt_line: {
+ parser.command = .{ .semantic_prompt = .init(.end_prompt_start_input_terminate_eol) };
+ if (data.len == 1) break :end_prompt_line;
+ if (data[1] != ';') break :valid;
+ parser.command.semantic_prompt.options_unvalidated = data[2..];
+ },
+
+ 'C' => end_input: {
+ parser.command = .{ .semantic_prompt = .init(.end_input_start_output) };
+ if (data.len == 1) break :end_input;
+ if (data[1] != ';') break :valid;
+ parser.command.semantic_prompt.options_unvalidated = data[2..];
+ },
+
+ 'D' => end_command: {
+ parser.command = .{ .semantic_prompt = .init(.end_command) };
+ if (data.len == 1) break :end_command;
+ if (data[1] != ';') break :valid;
+ parser.command.semantic_prompt.options_unvalidated = data[2..];
+ },
+
+ 'L' => {
+ if (data.len > 1) break :valid;
+ parser.command = .{ .semantic_prompt = .init(.fresh_line) };
+ },
+
+ 'N' => new_command: {
+ parser.command = .{ .semantic_prompt = .init(.new_command) };
+ if (data.len == 1) break :new_command;
+ if (data[1] != ';') break :valid;
+ parser.command.semantic_prompt.options_unvalidated = data[2..];
+ },
+
+ 'P' => prompt_start: {
+ parser.command = .{ .semantic_prompt = .init(.prompt_start) };
+ if (data.len == 1) break :prompt_start;
+ if (data[1] != ';') break :valid;
+ parser.command.semantic_prompt.options_unvalidated = data[2..];
+ },
+
+ else => break :valid,
+ }
+
+ return &parser.command;
+ }
+
+ // Any fallthroughs are invalid
+ parser.state = .invalid;
+ return null;
+}
+
+test "OSC 133: end_input_start_output" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;C";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+ try testing.expect(cmd.semantic_prompt.readOption(.aid) == null);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == null);
+}
+
+test "OSC 133: end_input_start_output extra contents" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;Cextra";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 133: end_input_start_output with options" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;C;aid=foo";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+ try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?);
+}
+
+test "OSC 133: end_input_start_output with cmdline" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline=echo bobr kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("echo bobr kurwa", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline 3" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline=echo bobr\\nkurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("echo bobr\nkurwa", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline 4" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline=$'echo bobr kurwa'";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("echo bobr kurwa", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline 5" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline='echo bobr kurwa'";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("echo bobr kurwa", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline 6" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline='echo bobr kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+ try testing.expectError(error.DecodeError, cmd.semantic_prompt.writeCommandLine(&w.writer));
+}
+
+test "OSC 133: end_input_start_output with cmdline 7" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline=$'echo bobr kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try testing.expectError(error.DecodeError, cmd.semantic_prompt.writeCommandLine(&w.writer));
+}
+
+test "OSC 133: end_input_start_output with cmdline 8" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline=$'";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+ try testing.expectError(error.DecodeError, cmd.semantic_prompt.writeCommandLine(&w.writer));
+}
+
+test "OSC 133: end_input_start_output with cmdline 9" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline=";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline_url 1" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline_url=echo bobr kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("echo bobr kurwa", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline_url 2" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline_url=echo bobr%20kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("echo bobr kurwa", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline_url 3" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline_url=echo bobr%3bkurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("echo bobr;kurwa", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline_url 4" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline_url=echo bobr%3kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+ try testing.expectError(error.DecodeError, cmd.semantic_prompt.writeCommandLine(&w.writer));
+}
+
+test "OSC 133: end_input_start_output with cmdline_url 5" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline_url=echo bobr%kurwa";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+ try testing.expectError(error.DecodeError, cmd.semantic_prompt.writeCommandLine(&w.writer));
+}
+
+test "OSC 133: end_input_start_output with cmdline_url 6" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline_url=echo bobr kurwa%20";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+
+ try cmd.semantic_prompt.writeCommandLine(&w.writer);
+ try testing.expectEqualStrings("echo bobr kurwa ", w.written());
+}
+
+test "OSC 133: end_input_start_output with cmdline_url 7" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline_url=echo bobr kurwa%2";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+ try testing.expectError(error.DecodeError, cmd.semantic_prompt.writeCommandLine(&w.writer));
+}
+
+test "OSC 133: end_input_start_output with cmdline_url 8" {
+ const testing = std.testing;
+
+ var w: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer w.deinit();
+
+ var p: Parser = .init(null);
+ const input = "133;C;cmdline_url=echo bobr kurwa%";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_input_start_output);
+ try testing.expectError(error.DecodeError, cmd.semantic_prompt.writeCommandLine(&w.writer));
+}
+
+test "OSC 133: fresh_line" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;L";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line);
+}
+
+test "OSC 133: fresh_line extra contents" {
+ const testing = std.testing;
+
+ // Random
+ {
+ var p: Parser = .init(null);
+ const input = "133;Lol";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+ }
+
+ // Options
+ {
+ var p: Parser = .init(null);
+ const input = "133;L;aid=foo";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+ }
+}
+
+test "OSC 133: fresh_line_new_prompt" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.aid) == null);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == null);
+}
+
+test "OSC 133: fresh_line_new_prompt with aid" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;aid=14";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expectEqualStrings("14", cmd.semantic_prompt.readOption(.aid).?);
+}
+
+test "OSC 133: fresh_line_new_prompt with '=' in aid" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;aid=a=b";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expectEqualStrings("a=b", cmd.semantic_prompt.readOption(.aid).?);
+}
+
+test "OSC 133: fresh_line_new_prompt with cl=line" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;cl=line";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line);
+}
+
+test "OSC 133: fresh_line_new_prompt with cl=m" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;cl=m";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == .multiple);
+}
+
+test "OSC 133: fresh_line_new_prompt with invalid cl" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;cl=invalid";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == null);
+}
+
+test "OSC 133: fresh_line_new_prompt with trailing ;" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+}
+
+test "OSC 133: fresh_line_new_prompt with bare key" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;barekey";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.aid) == null);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == null);
+}
+
+test "OSC 133: fresh_line_new_prompt with multiple options" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;aid=foo;cl=line";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line);
+}
+
+test "OSC 133: fresh_line_new_prompt default redraw" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.redraw) == null);
+}
+
+test "OSC 133: fresh_line_new_prompt with redraw=0" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;redraw=0";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == .false);
+}
+
+test "OSC 133: fresh_line_new_prompt with redraw=1" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;redraw=1";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == .true);
+}
+
+test "OSC 133: fresh_line_new_prompt with invalid redraw" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;A;redraw=x";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt);
+ try testing.expect(cmd.semantic_prompt.readOption(.redraw) == null);
+}
+
+test "OSC 133: prompt_start" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;P";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .prompt_start);
+ try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == null);
+}
+
+test "OSC 133: prompt_start with k=i" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;P;k=i";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .prompt_start);
+ try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .initial);
+}
+
+test "OSC 133: prompt_start with k=r" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;P;k=r";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .prompt_start);
+ try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .right);
+}
+
+test "OSC 133: prompt_start with k=c" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;P;k=c";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .prompt_start);
+ try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .continuation);
+}
+
+test "OSC 133: prompt_start with k=s" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;P;k=s";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .prompt_start);
+ try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .secondary);
+}
+
+test "OSC 133: prompt_start with invalid k" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;P;k=x";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .prompt_start);
+ try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == null);
+}
+
+test "OSC 133: prompt_start extra contents" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;Pextra";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 133: new_command" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;N";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .new_command);
+ try testing.expect(cmd.semantic_prompt.readOption(.aid) == null);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == null);
+}
+
+test "OSC 133: new_command with aid" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;N;aid=foo";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .new_command);
+ try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?);
+}
+
+test "OSC 133: new_command with cl=line" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;N;cl=line";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .new_command);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line);
+}
+
+test "OSC 133: new_command with multiple options" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;N;aid=foo;cl=line";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .new_command);
+ try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?);
+ try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line);
+}
+
+test "OSC 133: new_command extra contents" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;Nextra";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 133: end_prompt_start_input" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;B";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input);
+}
+
+test "OSC 133: end_prompt_start_input extra contents" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;Bextra";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 133: end_prompt_start_input with options" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;B;aid=foo";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input);
+ try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?);
+}
+
+test "OSC 133: end_prompt_start_input_terminate_eol" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;I";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input_terminate_eol);
+}
+
+test "OSC 133: end_prompt_start_input_terminate_eol extra contents" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;Iextra";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 133: end_prompt_start_input_terminate_eol with options" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;I;aid=foo";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input_terminate_eol);
+ try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?);
+}
+
+test "OSC 133: end_command" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;D";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_command);
+ try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == null);
+ try testing.expect(cmd.semantic_prompt.readOption(.aid) == null);
+ try testing.expect(cmd.semantic_prompt.readOption(.err) == null);
+}
+
+test "OSC 133: end_command extra contents" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+ const input = "133;Dextra";
+ for (input) |ch| p.next(ch);
+ try testing.expect(p.end(null) == null);
+}
+
+test "OSC 133: end_command with exit code 0" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;D;0";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_command);
+ try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == 0);
+}
+
+test "OSC 133: end_command with exit code and aid" {
+ const testing = std.testing;
+
+ var p: Parser = .init(null);
+
+ const input = "133;D;12;aid=foo";
+ for (input) |ch| p.next(ch);
+
+ const cmd = p.end(null).?.*;
+ try testing.expect(cmd == .semantic_prompt);
+ try testing.expect(cmd.semantic_prompt.action == .end_command);
+ try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?);
+ try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == 12);
+}
+
+test "Option.read aid" {
+ const testing = std.testing;
+ try testing.expectEqualStrings("test123", Option.aid.read("aid=test123").?);
+ try testing.expectEqualStrings("myaid", Option.aid.read("cl=line;aid=myaid;k=i").?);
+ try testing.expect(Option.aid.read("cl=line;k=i") == null);
+ try testing.expectEqualStrings("", Option.aid.read("aid=").?);
+ try testing.expectEqualStrings("last", Option.aid.read("k=i;aid=last").?);
+ try testing.expectEqualStrings("first", Option.aid.read("aid=first;k=i").?);
+ try testing.expect(Option.aid.read("") == null);
+ try testing.expect(Option.aid.read("aid") == null);
+ try testing.expectEqualStrings("value", Option.aid.read(";;aid=value;;").?);
+}
+
+test "Option.read cl" {
+ const testing = std.testing;
+ try testing.expect(Option.cl.read("cl=line").? == .line);
+ try testing.expect(Option.cl.read("cl=m").? == .multiple);
+ try testing.expect(Option.cl.read("cl=v").? == .conservative_vertical);
+ try testing.expect(Option.cl.read("cl=w").? == .smart_vertical);
+ try testing.expect(Option.cl.read("cl=invalid") == null);
+ try testing.expect(Option.cl.read("aid=foo") == null);
+}
+
+test "Option.read prompt_kind" {
+ const testing = std.testing;
+ try testing.expect(Option.prompt_kind.read("k=i").? == .initial);
+ try testing.expect(Option.prompt_kind.read("k=r").? == .right);
+ try testing.expect(Option.prompt_kind.read("k=c").? == .continuation);
+ try testing.expect(Option.prompt_kind.read("k=s").? == .secondary);
+ try testing.expect(Option.prompt_kind.read("k=x") == null);
+ try testing.expect(Option.prompt_kind.read("k=ii") == null);
+ try testing.expect(Option.prompt_kind.read("k=") == null);
+}
+
+test "Option.read err" {
+ const testing = std.testing;
+ try testing.expectEqualStrings("some_error", Option.err.read("err=some_error").?);
+ try testing.expect(Option.err.read("aid=foo") == null);
+}
+
+test "Option.read redraw" {
+ const testing = std.testing;
+ try testing.expect(Option.redraw.read("redraw=1").? == .true);
+ try testing.expect(Option.redraw.read("redraw=0").? == .false);
+ try testing.expect(Option.redraw.read("redraw=last").? == .last);
+ try testing.expect(Option.redraw.read("redraw=2") == null);
+ try testing.expect(Option.redraw.read("redraw=10") == null);
+ try testing.expect(Option.redraw.read("redraw=") == null);
+}
+
+test "Option.read special_key" {
+ const testing = std.testing;
+ try testing.expect(Option.special_key.read("special_key=1").? == true);
+ try testing.expect(Option.special_key.read("special_key=0").? == false);
+ try testing.expect(Option.special_key.read("special_key=x") == null);
+}
+
+test "Option.read click_events" {
+ const testing = std.testing;
+ try testing.expect(Option.click_events.read("click_events=1").? == true);
+ try testing.expect(Option.click_events.read("click_events=0").? == false);
+ try testing.expect(Option.click_events.read("click_events=yes") == null);
+}
+
+test "Option.read exit_code" {
+ const testing = std.testing;
+ try testing.expect(Option.exit_code.read("42").? == 42);
+ try testing.expect(Option.exit_code.read("0").? == 0);
+ try testing.expect(Option.exit_code.read("-1").? == -1);
+ try testing.expect(Option.exit_code.read("abc") == null);
+ try testing.expect(Option.exit_code.read("127;aid=foo").? == 127);
+}