diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/osc | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/osc')
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); +} |
