diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 56184df46..1f4489961 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -12,11 +12,9 @@ const mem = std.mem; const assert = @import("../quirks.zig").inlineAssert; const Allocator = mem.Allocator; const LibEnum = @import("../lib/enum.zig").Enum; -const RGB = @import("color.zig").RGB; const kitty_color = @import("kitty/color.zig"); -const osc_color = @import("osc/color.zig"); -const string_encoding = @import("../os/string_encoding.zig"); -pub const color = osc_color; +const parsers = @import("osc/parsers.zig"); +pub const color = parsers.color; const log = std.log.scoped(.osc); @@ -146,8 +144,8 @@ pub const Command = union(Key) { /// /// 4, 5, 10-19, 104, 105, 110-119 color_operation: struct { - op: osc_color.Operation, - requests: osc_color.List = .{}, + op: color.Operation, + requests: color.List = .{}, terminator: Terminator = .st, }, @@ -310,7 +308,7 @@ pub const Terminator = enum { pub const Parser = struct { /// Maximum size of a "normal" OSC. - const MAX_BUF = 2048; + pub const MAX_BUF = 2048; /// Optional allocator used to accept data longer than MAX_BUF. /// This only applies to some commands (e.g. OSC 52) that can @@ -646,9 +644,9 @@ pub const Parser = struct { .@"0", .@"2", - => self.parseChangeWindowTitle(terminator_ch), + => parsers.change_window_title.parse(self, terminator_ch), - .@"1" => self.parseChangeWindowIcon(terminator_ch), + .@"1" => parsers.change_window_icon.parse(self, terminator_ch), .@"4", .@"5", @@ -673,2330 +671,29 @@ pub const Parser = struct { .@"117", .@"118", .@"119", - => self.parseOscColor(terminator_ch), + => parsers.color.parse(self, terminator_ch), - .@"7" => self.parseReportPwd(terminator_ch), + .@"7" => parsers.report_pwd.parse(self, terminator_ch), - .@"8" => self.parseHyperlink(terminator_ch), + .@"8" => parsers.hyperlink.parse(self, terminator_ch), - .@"9" => self.parseOsc9(terminator_ch), + .@"9" => parsers.osc9.parse(self, terminator_ch), - .@"21" => self.parseKittyColorProtocol(terminator_ch), + .@"21" => parsers.kitty_color.parse(self, terminator_ch), - .@"22" => self.parseMouseShape(terminator_ch), + .@"22" => parsers.mouse_shape.parse(self, terminator_ch), - .@"52" => self.parseClipboardOperation(terminator_ch), + .@"52" => parsers.clipboard_operation.parse(self, terminator_ch), .@"77" => null, - .@"133" => self.parseSemanticPrompt(terminator_ch), + .@"133" => parsers.semantic_prompt.parse(self, terminator_ch), - .@"777" => self.parseRxvtExtension(terminator_ch), + .@"777" => parsers.rxvt_extension.parse(self, terminator_ch), }; } - - /// Parse OSC 0 and OSC 2 - fn parseChangeWindowTitle(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .change_window_title = data[0 .. data.len - 1 :0], - }; - return &self.command; - } - - /// Parse OSC 1 - fn parseChangeWindowIcon(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .change_window_icon = data[0 .. data.len - 1 :0], - }; - return &self.command; - } - - /// Parse OSCs 4, 5, 10-19, 104, 110-119 - fn parseOscColor(self: *Parser, terminator_ch: ?u8) ?*Command { - const alloc = self.alloc orelse { - self.state = .invalid; - return null; - }; - // If we've collected any extra data parse that, otherwise use an empty - // string. - const data = data: { - const writer = self.writer orelse break :data ""; - break :data writer.buffered(); - }; - // Check and make sure that we're parsing the correct OSCs - const op: osc_color.Operation = switch (self.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 => { - self.state = .invalid; - return null; - }, - }; - self.command = .{ - .color_operation = .{ - .op = op, - .requests = osc_color.parse(alloc, op, data) catch |err| list: { - log.info( - "failed to parse OSC {t} color request err={} data={s}", - .{ self.state, err, data }, - ); - break :list .{}; - }, - .terminator = .init(terminator_ch), - }, - }; - return &self.command; - } - - /// Parse OSC 7 - fn parseReportPwd(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .report_pwd = .{ - .value = data[0 .. data.len - 1 :0], - }, - }; - return &self.command; - } - - /// Parse OSC 8 hyperlinks - fn parseHyperlink(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - const s = std.mem.indexOfScalar(u8, data, ';') orelse { - self.state = .invalid; - return null; - }; - - self.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) self.command.hyperlink_start.id = value; - } else { - log.warn("unknown hyperlink option: '{s}'", .{key}); - } - kv_start = kv_end + 1; - } - - if (self.command.hyperlink_start.uri.len == 0) { - if (self.command.hyperlink_start.id != null) { - self.state = .invalid; - return null; - } - self.command = .hyperlink_end; - } - - return &self.command; - } - - /// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension. - fn parseOsc9(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - - // Check first to see if this is a ConEmu OSC - // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC - conemu: { - var data = writer.buffered(); - if (data.len == 0) break :conemu; - switch (data[0]) { - // Check for OSC 9;1 9;10 9;12 - '1' => { - if (data.len < 2) break :conemu; - switch (data[1]) { - // OSC 9;1 - ';' => { - self.command = .{ - .conemu_sleep = .{ - .duration_ms = if (std.fmt.parseUnsigned(u16, data[2..], 10)) |num| @min(num, 10_000) else |_| 100, - }, - }; - return &self.command; - }, - // OSC 9;10 - '0' => { - self.state = .invalid; - return null; - }, - // OSC 9;12 - '2' => { - self.command = .{ - .prompt_start = .{}, - }; - return &self.command; - }, - else => break :conemu, - } - }, - // OSC 9;2 - '2' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - data = writer.buffered(); - self.command = .{ - .conemu_show_message_box = data[2 .. data.len - 1 :0], - }; - return &self.command; - }, - // OSC 9;3 - '3' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - if (data.len == 2) { - self.command = .{ - .conemu_change_tab_title = .reset, - }; - return &self.command; - } - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - data = writer.buffered(); - self.command = .{ - .conemu_change_tab_title = .{ - .value = data[2 .. data.len - 1 :0], - }, - }; - return &self.command; - }, - // OSC 9;4 - '4' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - if (data.len < 3) break :conemu; - switch (data[2]) { - '0' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .remove, - }, - }; - }, - '1' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .set, - .progress = 0, - }, - }; - }, - '2' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .@"error", - }, - }; - }, - '3' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .indeterminate, - }, - }; - }, - '4' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .pause, - }, - }; - }, - else => break :conemu, - } - switch (self.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 - self.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 &self.command; - }, - // OSC 9;5 - '5' => { - self.command = .conemu_wait_input; - return &self.command; - }, - // OSC 9;6 - '6' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - data = writer.buffered(); - self.command = .{ - .conemu_guimacro = data[2 .. data.len - 1 :0], - }; - return &self.command; - }, - // OSC 9;7 - '7' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - self.state = .invalid; - return null; - }, - // OSC 9;8 - '8' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - self.state = .invalid; - return null; - }, - // OSC 9;9 - '9' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - self.state = .invalid; - return null; - }, - else => break :conemu, - } - } - - // If it's not a ConEmu OSC, it's an iTerm2 notification - - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .show_desktop_notification = .{ - .title = "", - .body = data[0 .. data.len - 1 :0], - }, - }; - return &self.command; - } - - /// Parse OSC 21, the Kitty Color Protocol. - fn parseKittyColorProtocol(self: *Parser, terminator_ch: ?u8) ?*Command { - assert(self.state == .@"21"); - const alloc = self.alloc orelse { - self.state = .invalid; - return null; - }; - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - self.command = .{ - .kitty_color_protocol = .{ - .list = .empty, - .terminator = .init(terminator_ch), - }, - }; - const list = &self.command.kitty_color_protocol.list; - const data = writer.buffered(); - 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", .{}); - self.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 (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 &self.command; - } - - // Parse OSC 22 - fn parseMouseShape(self: *Parser, _: ?u8) ?*Command { - assert(self.state == .@"22"); - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .mouse_shape = .{ - .value = data[0 .. data.len - 1 :0], - }, - }; - return &self.command; - } - - /// Parse OSC 52 - fn parseClipboardOperation(self: *Parser, _: ?u8) ?*Command { - assert(self.state == .@"52"); - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - if (data.len == 1) { - self.state = .invalid; - return null; - } - if (data[0] == ';') { - self.command = .{ - .clipboard_contents = .{ - .kind = 'c', - .data = data[1 .. data.len - 1 :0], - }, - }; - } else { - if (data.len < 2) { - self.state = .invalid; - return null; - } - if (data[1] != ';') { - self.state = .invalid; - return null; - } - self.command = .{ - .clipboard_contents = .{ - .kind = data[0], - .data = data[2 .. data.len - 1 :0], - }, - }; - } - return &self.command; - } - - /// Parse OSC 133, semantic prompts - fn parseSemanticPrompt(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - if (data.len == 0) { - self.state = .invalid; - return null; - } - switch (data[0]) { - 'A' => prompt_start: { - self.command = .{ - .prompt_start = .{}, - }; - if (data.len == 1) break :prompt_start; - if (data[1] != ';') { - self.state = .invalid; - return null; - } - var it = SemanticPromptKVIterator.init(writer) catch { - self.state = .invalid; - return null; - }; - while (it.next()) |kv| { - if (std.mem.eql(u8, kv.key, "aid")) { - self.command.prompt_start.aid = kv.value; - } else if (std.mem.eql(u8, kv.key, "redraw")) redraw: { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - // Kitty supports a "redraw" option for prompt_start. I can't find - // this documented anywhere but can see in the code that this is used - // by shell environments to tell the terminal that the shell will NOT - // redraw the prompt so we should attempt to resize it. - self.command.prompt_start.redraw = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { - '0' => break :value false, - '1' => break :value true, - else => break :value null, - } - }) orelse { - log.info("OSC 133 A: invalid redraw value: {s}", .{kv.value}); - break :redraw; - }; - } else if (std.mem.eql(u8, kv.key, "special_key")) redraw: { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - self.command.prompt_start.special_key = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { - '0' => break :value false, - '1' => break :value true, - else => break :value null, - } - }) orelse { - log.info("OSC 133 A invalid special_key value: {s}", .{kv.value}); - break :redraw; - }; - } else if (std.mem.eql(u8, kv.key, "click_events")) redraw: { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - self.command.prompt_start.click_events = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { - '0' => break :value false, - '1' => break :value true, - else => break :value null, - } - }) orelse { - log.info("OSC 133 A invalid click_events value: {s}", .{kv.value}); - break :redraw; - }; - } else if (std.mem.eql(u8, kv.key, "k")) k: { - // The "k" marks the kind of prompt, or "primary" if we don't know. - // This can be used to distinguish between the first (initial) prompt, - // a continuation, etc. - if (kv.value.len != 1) break :k; - self.command.prompt_start.kind = switch (kv.value[0]) { - 'c' => .continuation, - 's' => .secondary, - 'r' => .right, - 'i' => .primary, - else => .primary, - }; - } else log.info("OSC 133 A: unknown semantic prompt option: {s}", .{kv.key}); - } - }, - 'B' => prompt_end: { - self.command = .prompt_end; - if (data.len == 1) break :prompt_end; - if (data[1] != ';') { - self.state = .invalid; - return null; - } - var it = SemanticPromptKVIterator.init(writer) catch { - self.state = .invalid; - return null; - }; - while (it.next()) |kv| { - log.info("OSC 133 B: unknown semantic prompt option: {s}", .{kv.key}); - } - }, - 'C' => end_of_input: { - self.command = .{ - .end_of_input = .{}, - }; - if (data.len == 1) break :end_of_input; - if (data[1] != ';') { - self.state = .invalid; - return null; - } - var it = SemanticPromptKVIterator.init(writer) catch { - self.state = .invalid; - return null; - }; - while (it.next()) |kv| { - if (std.mem.eql(u8, kv.key, "cmdline")) { - self.command.end_of_input.cmdline = string_encoding.printfQDecode(kv.value) catch null; - } else if (std.mem.eql(u8, kv.key, "cmdline_url")) { - self.command.end_of_input.cmdline = string_encoding.urlPercentDecode(kv.value) catch null; - } else { - log.info("OSC 133 C: unknown semantic prompt option: {s}", .{kv.key}); - } - } - }, - 'D' => { - const exit_code: ?u8 = exit_code: { - if (data.len == 1) break :exit_code null; - if (data[1] != ';') { - self.state = .invalid; - return null; - } - break :exit_code std.fmt.parseUnsigned(u8, data[2..], 10) catch null; - }; - self.command = .{ - .end_of_command = .{ - .exit_code = exit_code, - }, - }; - }, - else => { - self.state = .invalid; - return null; - }, - } - return &self.command; - } - - const SemanticPromptKVIterator = struct { - index: usize, - string: []u8, - - pub const SemanticPromptKV = struct { - key: [:0]u8, - value: [:0]u8, - }; - - pub fn init(writer: *std.Io.Writer) std.Io.Writer.Error!SemanticPromptKVIterator { - // add a semicolon to make it easier to find and sentinel terminate the values - try writer.writeByte(';'); - return .{ - .index = 0, - .string = writer.buffered()[2..], - }; - } - - pub fn next(self: *SemanticPromptKVIterator) ?SemanticPromptKV { - if (self.index >= self.string.len) return null; - - const kv = kv: { - const index = std.mem.indexOfScalarPos(u8, self.string, self.index, ';') orelse { - self.index = self.string.len; - return null; - }; - self.string[index] = 0; - const kv = self.string[self.index..index :0]; - self.index = index + 1; - break :kv kv; - }; - - const key = key: { - const index = std.mem.indexOfScalar(u8, kv, '=') orelse break :key kv; - kv[index] = 0; - const key = kv[0..index :0]; - break :key key; - }; - - const value = kv[key.len + 1 .. :0]; - - return .{ - .key = key, - .value = value, - }; - } - }; - - /// Parse OSC 777 - fn parseRxvtExtension(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - // ensure that we are sentinel terminated - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - const k = std.mem.indexOfScalar(u8, data, ';') orelse { - self.state = .invalid; - return null; - }; - const ext = data[0..k]; - if (!std.mem.eql(u8, ext, "notify")) { - log.warn("unknown rxvt extension: {s}", .{ext}); - self.state = .invalid; - return null; - } - const t = std.mem.indexOfScalarPos(u8, data, k + 1, ';') orelse { - log.warn("rxvt notify extension is missing the title", .{}); - self.state = .invalid; - return null; - }; - data[t] = 0; - const title = data[k + 1 .. t :0]; - const body = data[t + 1 .. data.len - 1 :0]; - self.command = .{ - .show_desktop_notification = .{ - .title = title, - .body = body, - }, - }; - return &self.command; - } }; test { - _ = osc_color; -} - -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 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); -} - -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); -} - -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); -} - -// See src/terminal/osc/color.zig for more OSC 4 tests. - -// See src/terminal/osc/color.zig for OSC 5 tests. - -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); -} - -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); -} - -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); -} - -// See src/terminal/osc/color.zig for OSC 10 tests. - -// See src/terminal/osc/color.zig for OSC 11 tests. - -// See src/terminal/osc/color.zig for OSC 12 tests. - -// See src/terminal/osc/color.zig for OSC 13 tests. - -// See src/terminal/osc/color.zig for OSC 14 tests. - -// See src/terminal/osc/color.zig for OSC 15 tests. - -// See src/terminal/osc/color.zig for OSC 16 tests. - -// See src/terminal/osc/color.zig for OSC 17 tests. - -// See src/terminal/osc/color.zig for OSC 18 tests. - -// See src/terminal/osc/color.zig for OSC 19 tests. - -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); -} - -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); -} - -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); -} - -// See src/terminal/osc/color.zig for OSC 104 tests. - -// See src/terminal/osc/color.zig for OSC 105 tests. - -// See src/terminal/osc/color.zig for OSC 110 tests. - -// See src/terminal/osc/color.zig for OSC 111 tests. - -// See src/terminal/osc/color.zig for OSC 112 tests. - -// See src/terminal/osc/color.zig for OSC 113 tests. - -// See src/terminal/osc/color.zig for OSC 114 tests. - -// See src/terminal/osc/color.zig for OSC 115 tests. - -// See src/terminal/osc/color.zig for OSC 116 tests. - -// See src/terminal/osc/color.zig for OSC 117 tests. - -// See src/terminal/osc/color.zig for OSC 118 tests. - -// See src/terminal/osc/color.zig for OSC 119 tests. - -test "OSC 133: prompt_start" { - 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 == .prompt_start); - try testing.expect(cmd.prompt_start.aid == null); - try testing.expect(cmd.prompt_start.redraw); -} - -test "OSC 133: prompt_start with single option" { - 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 == .prompt_start); - try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); -} - -test "OSC 133: prompt_start with '=' in aid" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;aid=a=b;redraw=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expectEqualStrings("a=b", cmd.prompt_start.aid.?); - try testing.expect(!cmd.prompt_start.redraw); -} - -test "OSC 133: prompt_start with redraw disabled" { - 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 == .prompt_start); - try testing.expect(!cmd.prompt_start.redraw); -} - -test "OSC 133: prompt_start with redraw invalid value" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;redraw=42"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.redraw); - try testing.expect(cmd.prompt_start.kind == .primary); -} - -test "OSC 133: prompt_start with continuation" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;k=c"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.kind == .continuation); -} - -test "OSC 133: prompt_start with secondary" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;k=s"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.kind == .secondary); -} - -test "OSC 133: prompt_start with special_key" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;special_key=1"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.special_key == true); -} - -test "OSC 133: prompt_start with special_key invalid" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;special_key=bobr"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.special_key == false); -} - -test "OSC 133: prompt_start with special_key 0" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;special_key=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.special_key == false); -} - -test "OSC 133: prompt_start with special_key empty" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;special_key="; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.special_key == false); -} - -test "OSC 133: prompt_start with click_events true" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;click_events=1"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.click_events == true); -} - -test "OSC 133: prompt_start with click_events false" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;click_events=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.click_events == false); -} - -test "OSC 133: prompt_start with click_events empty" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;click_events="; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.click_events == false); -} - -test "OSC 133: end_of_command no exit code" { - 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 == .end_of_command); -} - -test "OSC 133: end_of_command with exit code" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;D;25"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_command); - try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); -} - -test "OSC 133: prompt_end" { - 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 == .prompt_end); -} - -test "OSC 133: end_of_input" { - 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 == .end_of_input); -} - -test "OSC 133: end_of_input with cmdline 1" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 2" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 3" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr\nkurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 4" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 5" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 6" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline 7" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline 8" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline 9" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline 10" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 1" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 2" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 3" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr;kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 4" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline_url 5" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline_url 6" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline_url 7" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa ", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 8" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline_url 9" { - const testing = std.testing; - - 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 == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -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"); + _ = parsers; } diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig new file mode 100644 index 000000000..152276af2 --- /dev/null +++ b/src/terminal/osc/parsers.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +pub const change_window_icon = @import("parsers/change_window_icon.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 kitty_color = @import("parsers/kitty_color.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 { + _ = change_window_icon; + _ = change_window_title; + _ = clipboard_operation; + _ = color; + _ = hyperlink; + _ = kitty_color; + _ = mouse_shape; + _ = osc9; + _ = report_pwd; + _ = rxvt_extension; + _ = semantic_prompt; +} diff --git a/src/terminal/osc/parsers/change_window_icon.zig b/src/terminal/osc/parsers/change_window_icon.zig new file mode 100644 index 000000000..aefe17696 --- /dev/null +++ b/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 writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + 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/src/terminal/osc/parsers/change_window_title.zig b/src/terminal/osc/parsers/change_window_title.zig new file mode 100644 index 000000000..b0bf44dd3 --- /dev/null +++ b/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 writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + 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/src/terminal/osc/parsers/clipboard_operation.zig b/src/terminal/osc/parsers/clipboard_operation.zig new file mode 100644 index 000000000..59a8831bc --- /dev/null +++ b/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 writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + 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/src/terminal/osc/color.zig b/src/terminal/osc/parsers/color.zig similarity index 86% rename from src/terminal/osc/color.zig rename to src/terminal/osc/parsers/color.zig index 9fd81ed63..7d3dc68c0 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/parsers/color.zig @@ -1,10 +1,15 @@ 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; -pub const ParseError = Allocator.Error || error{ +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, }; @@ -36,6 +41,76 @@ pub const Operation = enum { 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 writer = parser.writer orelse break :data ""; + break :data writer.buffered(); + }; + // 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. @@ -46,7 +121,7 @@ pub const Operation = enum { /// request) but grants us an easier to understand and testable implementation. /// /// If color changing ends up being a bottleneck we can optimize this later. -pub fn parse( +fn parseColor( alloc: Allocator, op: Operation, buf: []const u8, @@ -295,7 +370,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -317,7 +392,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -336,7 +411,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -360,7 +435,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -387,7 +462,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -419,7 +494,7 @@ test "OSC 5:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_5, body); + var list = try parseColor(alloc, .osc_5, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -439,7 +514,7 @@ test "OSC 4: multiple requests" { // printf '\e]4;0;red;1;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_4, "0;red;1;blue", @@ -465,7 +540,7 @@ test "OSC 4: multiple requests" { // Multiple requests with same index overwrite each other // printf '\e]4;0;red;0;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_4, "0;red;0;blue", @@ -505,7 +580,7 @@ test "OSC 104:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_104, body); + var list = try parseColor(alloc, .osc_104, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -529,7 +604,7 @@ test "OSC 104:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_104, body); + var list = try parseColor(alloc, .osc_104, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -544,7 +619,7 @@ test "OSC 104: empty index" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, "0;;1"); + var list = try parseColor(alloc, .osc_104, "0;;1"); defer list.deinit(alloc); try testing.expectEqual(2, list.count()); try testing.expectEqual( @@ -561,7 +636,7 @@ test "OSC 104: invalid index" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, "ffff;1"); + var list = try parseColor(alloc, .osc_104, "ffff;1"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -574,7 +649,7 @@ test "OSC 104: reset all" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, ""); + var list = try parseColor(alloc, .osc_104, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -587,7 +662,7 @@ test "OSC 105: reset all" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_105, ""); + var list = try parseColor(alloc, .osc_105, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -611,7 +686,7 @@ test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: // Example script: // printf '\e]10;red\e\\' { - var list = try parse(alloc, op, "red"); + var list = try parseColor(alloc, op, "red"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -632,7 +707,7 @@ test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: // Example script: // printf '\e]11;red;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_11, "red;blue", @@ -671,7 +746,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // Example script: // printf '\e]110\e\\' { - var list = try parse(alloc, op, ""); + var list = try parseColor(alloc, op, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -684,7 +759,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // // printf '\e]110;\e\\' { - var list = try parse(alloc, op, ";"); + var list = try parseColor(alloc, op, ";"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -697,7 +772,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // // printf '\e]110 \e\\' { - var list = try parse(alloc, op, " "); + var list = try parseColor(alloc, op, " "); defer list.deinit(alloc); try testing.expectEqual(0, list.count()); } diff --git a/src/terminal/osc/parsers/hyperlink.zig b/src/terminal/osc/parsers/hyperlink.zig new file mode 100644 index 000000000..cf328beb5 --- /dev/null +++ b/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 writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + 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/src/terminal/osc/parsers/kitty_color.zig b/src/terminal/osc/parsers/kitty_color.zig new file mode 100644 index 000000000..30a7fe77f --- /dev/null +++ b/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 writer = parser.writer orelse { + 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 = writer.buffered(); + 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/src/terminal/osc/parsers/mouse_shape.zig b/src/terminal/osc/parsers/mouse_shape.zig new file mode 100644 index 000000000..91c5ab270 --- /dev/null +++ b/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 writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + 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/src/terminal/osc/parsers/osc9.zig b/src/terminal/osc/parsers/osc9.zig new file mode 100644 index 000000000..1ca7ba5a0 --- /dev/null +++ b/src/terminal/osc/parsers/osc9.zig @@ -0,0 +1,766 @@ +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 writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + + // Check first to see if this is a ConEmu OSC + // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + conemu: { + var data = writer.buffered(); + if (data.len == 0) break :conemu; + switch (data[0]) { + // Check for OSC 9;1 9;10 9;12 + '1' => { + if (data.len < 2) break :conemu; + switch (data[1]) { + // OSC 9;1 + ';' => { + 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 + '0' => { + parser.state = .invalid; + return null; + }, + // OSC 9;12 + '2' => { + parser.command = .{ + .prompt_start = .{}, + }; + return &parser.command; + }, + else => break :conemu, + } + }, + // OSC 9;2 + '2' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_show_message_box = data[2 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;3 + '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 = writer.buffered(); + parser.command = .{ + .conemu_change_tab_title = .{ + .value = data[2 .. data.len - 1 :0], + }, + }; + return &parser.command; + }, + // OSC 9;4 + '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 + '5' => { + parser.command = .conemu_wait_input; + return &parser.command; + }, + // OSC 9;6 + '6' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_guimacro = data[2 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;7 + '7' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + parser.state = .invalid; + return null; + }, + // OSC 9;8 + '8' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + parser.state = .invalid; + return null; + }, + // OSC 9;9 + '9' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + parser.state = .invalid; + return null; + }, + 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 = writer.buffered(); + 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); +} diff --git a/src/terminal/osc/parsers/report_pwd.zig b/src/terminal/osc/parsers/report_pwd.zig new file mode 100644 index 000000000..080b9cbb0 --- /dev/null +++ b/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 writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + 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/src/terminal/osc/parsers/rxvt_extension.zig b/src/terminal/osc/parsers/rxvt_extension.zig new file mode 100644 index 000000000..94a0961d2 --- /dev/null +++ b/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 writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + // ensure that we are sentinel terminated + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + 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/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig new file mode 100644 index 000000000..510fe3447 --- /dev/null +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -0,0 +1,694 @@ +const std = @import("std"); + +const string_encoding = @import("../../../os/string_encoding.zig"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_semantic_prompt); + +/// Parse OSC 133, semantic prompts +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 0) { + parser.state = .invalid; + return null; + } + switch (data[0]) { + 'A' => prompt_start: { + parser.command = .{ + .prompt_start = .{}, + }; + if (data.len == 1) break :prompt_start; + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + var it = SemanticPromptKVIterator.init(writer) catch { + parser.state = .invalid; + return null; + }; + while (it.next()) |kv| { + if (std.mem.eql(u8, kv.key, "aid")) { + parser.command.prompt_start.aid = kv.value; + } else if (std.mem.eql(u8, kv.key, "redraw")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + // Kitty supports a "redraw" option for prompt_start. I can't find + // this documented anywhere but can see in the code that this is used + // by shell environments to tell the terminal that the shell will NOT + // redraw the prompt so we should attempt to resize it. + parser.command.prompt_start.redraw = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A: invalid redraw value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "special_key")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + parser.command.prompt_start.special_key = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A invalid special_key value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "click_events")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + parser.command.prompt_start.click_events = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A invalid click_events value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "k")) k: { + // The "k" marks the kind of prompt, or "primary" if we don't know. + // This can be used to distinguish between the first (initial) prompt, + // a continuation, etc. + if (kv.value.len != 1) break :k; + parser.command.prompt_start.kind = switch (kv.value[0]) { + 'c' => .continuation, + 's' => .secondary, + 'r' => .right, + 'i' => .primary, + else => .primary, + }; + } else log.info("OSC 133 A: unknown semantic prompt option: {s}", .{kv.key}); + } + }, + 'B' => prompt_end: { + parser.command = .prompt_end; + if (data.len == 1) break :prompt_end; + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + var it = SemanticPromptKVIterator.init(writer) catch { + parser.state = .invalid; + return null; + }; + while (it.next()) |kv| { + log.info("OSC 133 B: unknown semantic prompt option: {s}", .{kv.key}); + } + }, + 'C' => end_of_input: { + parser.command = .{ + .end_of_input = .{}, + }; + if (data.len == 1) break :end_of_input; + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + var it = SemanticPromptKVIterator.init(writer) catch { + parser.state = .invalid; + return null; + }; + while (it.next()) |kv| { + if (std.mem.eql(u8, kv.key, "cmdline")) { + parser.command.end_of_input.cmdline = string_encoding.printfQDecode(kv.value) catch null; + } else if (std.mem.eql(u8, kv.key, "cmdline_url")) { + parser.command.end_of_input.cmdline = string_encoding.urlPercentDecode(kv.value) catch null; + } else { + log.info("OSC 133 C: unknown semantic prompt option: {s}", .{kv.key}); + } + } + }, + 'D' => { + const exit_code: ?u8 = exit_code: { + if (data.len == 1) break :exit_code null; + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + break :exit_code std.fmt.parseUnsigned(u8, data[2..], 10) catch null; + }; + parser.command = .{ + .end_of_command = .{ + .exit_code = exit_code, + }, + }; + }, + else => { + parser.state = .invalid; + return null; + }, + } + return &parser.command; +} + +const SemanticPromptKVIterator = struct { + index: usize, + string: []u8, + + pub const SemanticPromptKV = struct { + key: [:0]u8, + value: [:0]u8, + }; + + pub fn init(writer: *std.Io.Writer) std.Io.Writer.Error!SemanticPromptKVIterator { + // add a semicolon to make it easier to find and sentinel terminate the values + try writer.writeByte(';'); + return .{ + .index = 0, + .string = writer.buffered()[2..], + }; + } + + pub fn next(self: *SemanticPromptKVIterator) ?SemanticPromptKV { + if (self.index >= self.string.len) return null; + + const kv = kv: { + const index = std.mem.indexOfScalarPos(u8, self.string, self.index, ';') orelse { + self.index = self.string.len; + return null; + }; + self.string[index] = 0; + const kv = self.string[self.index..index :0]; + self.index = index + 1; + break :kv kv; + }; + + const key = key: { + const index = std.mem.indexOfScalar(u8, kv, '=') orelse break :key kv; + kv[index] = 0; + const key = kv[0..index :0]; + break :key key; + }; + + const value = kv[key.len + 1 .. :0]; + + return .{ + .key = key, + .value = value, + }; + } +}; + +test "OSC 133: prompt_start" { + 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 == .prompt_start); + try testing.expect(cmd.prompt_start.aid == null); + try testing.expect(cmd.prompt_start.redraw); +} + +test "OSC 133: prompt_start with single option" { + 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 == .prompt_start); + try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); +} + +test "OSC 133: prompt_start with '=' in aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=a=b;redraw=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expectEqualStrings("a=b", cmd.prompt_start.aid.?); + try testing.expect(!cmd.prompt_start.redraw); +} + +test "OSC 133: prompt_start with redraw disabled" { + 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 == .prompt_start); + try testing.expect(!cmd.prompt_start.redraw); +} + +test "OSC 133: prompt_start with redraw invalid value" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;redraw=42"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.redraw); + try testing.expect(cmd.prompt_start.kind == .primary); +} + +test "OSC 133: prompt_start with continuation" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;k=c"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.kind == .continuation); +} + +test "OSC 133: prompt_start with secondary" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;k=s"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.kind == .secondary); +} + +test "OSC 133: prompt_start with special_key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;special_key=1"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.special_key == true); +} + +test "OSC 133: prompt_start with special_key invalid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;special_key=bobr"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.special_key == false); +} + +test "OSC 133: prompt_start with special_key 0" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;special_key=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.special_key == false); +} + +test "OSC 133: prompt_start with special_key empty" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;special_key="; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.special_key == false); +} + +test "OSC 133: prompt_start with click_events true" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;click_events=1"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.click_events == true); +} + +test "OSC 133: prompt_start with click_events false" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;click_events=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.click_events == false); +} + +test "OSC 133: prompt_start with click_events empty" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;click_events="; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.click_events == false); +} + +test "OSC 133: end_of_command no exit code" { + 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 == .end_of_command); +} + +test "OSC 133: end_of_command with exit code" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;D;25"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_command); + try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); +} + +test "OSC 133: prompt_end" { + 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 == .prompt_end); +} + +test "OSC 133: end_of_input" { + 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 == .end_of_input); +} + +test "OSC 133: end_of_input with cmdline 1" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 2" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 3" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr\nkurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 4" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 5" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline 6" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline 7" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline 8" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline 9" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline 10" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 1" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 2" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 3" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr;kurwa", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 4" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline_url 5" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline_url 6" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline_url 7" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline != null); + try testing.expectEqualStrings("echo bobr kurwa ", cmd.end_of_input.cmdline.?); +} + +test "OSC 133: end_of_input with cmdline_url 8" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} + +test "OSC 133: end_of_input with cmdline_url 9" { + const testing = std.testing; + + 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 == .end_of_input); + try testing.expect(cmd.end_of_input.cmdline == null); +} diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index c33dba1bb..1ee4f3f08 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -4,7 +4,7 @@ const stream = @import("stream.zig"); const Action = stream.Action; const Screen = @import("Screen.zig"); const modes = @import("modes.zig"); -const osc_color = @import("osc/color.zig"); +const osc_color = @import("osc/parsers/color.zig"); const kitty_color = @import("kitty/color.zig"); const Terminal = @import("Terminal.zig");