From fb1268a9089c3be639241402016842fb9e4c9f0d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 17 Dec 2025 12:57:27 -0600 Subject: [PATCH 1/6] benchmark: add doNotOptimizeAway to OSC benchmark --- src/benchmark/OscParser.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/benchmark/OscParser.zig b/src/benchmark/OscParser.zig index 6243aba7d..bd82a3534 100644 --- a/src/benchmark/OscParser.zig +++ b/src/benchmark/OscParser.zig @@ -101,7 +101,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { }; for (osc_buf[0..len]) |c| self.parser.next(c); - _ = self.parser.end(std.ascii.control_code.bel); + std.mem.doNotOptimizeAway(self.parser.end(std.ascii.control_code.bel)); self.parser.reset(); } } From d32a94a06ac3172fc86d0b366a48dbcee853daa6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 17 Dec 2025 17:36:18 -0600 Subject: [PATCH 2/6] core: add new OSC parser This replaces the OSC parser with one that only uses a state machine to determine which OSC is being handled, rather than parsing the whole OSC. Once the OSC command is determined the remainder of the data is stored in a buffer until the terminator is found. The data is then parsed to determine the final OSC command. --- src/terminal/kitty/color.zig | 4 + src/terminal/osc.zig | 2226 +++++++++++++++------------------- 2 files changed, 986 insertions(+), 1244 deletions(-) diff --git a/src/terminal/kitty/color.zig b/src/terminal/kitty/color.zig index deeabcfb7..c1072c390 100644 --- a/src/terminal/kitty/color.zig +++ b/src/terminal/kitty/color.zig @@ -17,6 +17,10 @@ pub const OSC = struct { /// request. terminator: Terminator = .st, + pub fn deinit(self: *OSC, alloc: std.mem.Allocator) void { + self.list.deinit(alloc); + } + /// We don't currently support encoding this to C in any way. pub const C = void; diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index f62b7a6cd..d81244b9f 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -309,170 +309,90 @@ pub const Terminator = enum { }; pub const Parser = struct { + /// Maximum size of a "normal" OSC. + 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 /// reasonably exceed MAX_BUF. - alloc: ?Allocator, + alloc: ?Allocator = null, /// Current state of the parser. - state: State, + state: State = .start, - /// Current command of the parser, this accumulates. - command: Command, + /// Buffer for temporary storage of OSC data + buffer: [MAX_BUF]u8 = undefined, + /// Fixed writer for accumulating OSC data + fixed: ?std.Io.Writer = null, + /// Allocating writer for accumulating OSC data + allocating: ?std.Io.Writer.Allocating = null, + /// Pointer to the active writer for accumulating OSC data + writer: ?*std.Io.Writer = null, - /// Buffer that stores the input we see for a single OSC command. - /// Slices in Command are offsets into this buffer. - buf: [MAX_BUF]u8, - buf_start: usize, - buf_idx: usize, - buf_dynamic: ?*std.ArrayListUnmanaged(u8), - - /// True when a command is complete/valid to return. - complete: bool, - - /// Temporary state that is dependent on the current state. - temp_state: union { - /// Current string parameter being populated - str: *[:0]const u8, - - /// Current numeric parameter being populated - num: u16, - - /// Temporary state for key/value pairs - key: []const u8, - }, - - // Maximum length of a single OSC command. This is the full OSC command - // sequence length (excluding ESC ]). This is arbitrary, I couldn't find - // any definitive resource on how long this should be. - // - // NOTE: This does mean certain OSC sequences such as OSC 8 (hyperlinks) - // won't work if their parameters are larger than fit in the buffer. - const MAX_BUF = 2048; + /// The command that is the result of parsing. + command: Command = .invalid, pub const State = enum { - empty, + start, invalid, - swallow, - // Command prefixes. We could just accumulate and compare (mem.eql) - // but the state space is small enough that we just build it up this way. + // OSC command prefixes. Not all of these are valid OSCs, but may be + // needed to "bridge" to a valid OSC (e.g. to support OSC 777 we need to + // have a state "77" even though there is no OSC 77). @"0", @"1", + @"2", + @"4", + @"5", + @"7", + @"8", + @"9", @"10", - @"104", @"11", @"12", @"13", - @"133", @"14", @"15", @"16", @"17", @"18", @"19", - @"2", @"21", @"22", - @"4", - @"5", @"52", - @"7", @"77", + @"104", + @"110", + @"111", + @"112", + @"113", + @"114", + @"115", + @"116", + @"117", + @"118", + @"119", + @"133", @"777", - @"8", - @"9", - - // We're in a semantic prompt OSC command but we aren't sure - // what the command is yet, i.e. `133;` - semantic_prompt, - semantic_option_start, - semantic_option_key, - semantic_option_value, - semantic_exit_code_start, - semantic_exit_code, - - // Get/set clipboard states - clipboard_kind, - clipboard_kind_end, - - // OSC color operation. - osc_color, - - // Hyperlinks - hyperlink_param_key, - hyperlink_param_value, - hyperlink_uri, - - // rxvt extension. Only used for OSC 777 and only the value "notify" is - // supported - rxvt_extension, - - // Title of a desktop notification - notification_title, - - // Expect a string parameter. param_str must be set as well as - // buf_start. - string, - - // A string that can grow beyond MAX_BUF. This uses the allocator. - // If the parser has no allocator then it is treated as if the - // buffer is full. - allocable_string, - - // Kitty color protocol - // https://sw.kovidgoyal.net/kitty/color-stack/#id1 - kitty_color_protocol_key, - kitty_color_protocol_value, - - // OSC 9 is used by ConEmu and iTerm2 for different things. - // iTerm2 uses it to post a notification[1]. - // ConEmu uses it to implement many custom functions[2]. - // - // Some Linux applications (namely systemd and flatpak) have - // adopted the ConEmu implementation but this causes bogus - // notifications on iTerm2 compatible terminal emulators. - // - // Ghostty supports both by disallowing ConEmu-specific commands - // from being shown as desktop notifications. - // - // [1]: https://iterm2.com/documentation-escape-codes.html - // [2]: https://conemu.github.io/en/AnsiEscapeCodes.html#OSC_Operating_system_commands - osc_9, - - // ConEmu specific substates - conemu_sleep, - conemu_sleep_value, - conemu_message_box, - conemu_tab, - conemu_tab_txt, - conemu_progress_prestate, - conemu_progress_state, - conemu_progress_prevalue, - conemu_progress_value, - conemu_guimacro, }; pub fn init(alloc: ?Allocator) Parser { var result: Parser = .{ .alloc = alloc, - .state = .empty, + .state = .start, + .fixed = null, + .allocating = null, + .writer = null, .command = .invalid, - .buf_start = 0, - .buf_idx = 0, - .buf_dynamic = null, - .complete = false, // Keeping all our undefined values together so we can // visually easily duplicate them in the Valgrind check below. - .buf = undefined, - .temp_state = undefined, + .buffer = undefined, }; if (std.valgrind.runningOnValgrind() > 0) { // Initialize our undefined fields so Valgrind can catch it. // https://github.com/ziglang/zig/issues/19148 - result.buf = undefined; - result.temp_state = undefined; + result.buffer = undefined; } return result; @@ -485,75 +405,108 @@ pub const Parser = struct { /// Reset the parser state. pub fn reset(self: *Parser) void { - // If the state is already empty then we do nothing because - // we may touch uninitialized memory. - if (self.state == .empty) { - assert(self.buf_start == 0); - assert(self.buf_idx == 0); - assert(!self.complete); - assert(self.buf_dynamic == null); - return; - } + // If we set up an allocating writer, free up that memory. + if (self.allocating) |*allocating| allocating.deinit(); - // Some commands have their own memory management we need to clear. + // Handle any cleanup that individual OSCs require. switch (self.command) { - .kitty_color_protocol => |*v| v.list.deinit(self.alloc.?), - .color_operation => |*v| v.requests.deinit(self.alloc.?), - else => {}, + .kitty_color_protocol => |*v| kitty_color_protocol: { + v.deinit(self.alloc orelse break :kitty_color_protocol); + }, + .change_window_icon, + .change_window_title, + .clipboard_contents, + .color_operation, + .conemu_change_tab_title, + .conemu_guimacro, + .conemu_progress_report, + .conemu_show_message_box, + .conemu_sleep, + .conemu_wait_input, + .end_of_command, + .end_of_input, + .hyperlink_end, + .hyperlink_start, + .invalid, + .mouse_shape, + .prompt_end, + .prompt_start, + .report_pwd, + .show_desktop_notification, + => {}, } - self.state = .empty; - self.buf_start = 0; - self.buf_idx = 0; + self.state = .start; + self.fixed = null; + self.allocating = null; + self.writer = null; self.command = .invalid; - self.complete = false; - if (self.buf_dynamic) |ptr| { - const alloc = self.alloc.?; - ptr.deinit(alloc); - alloc.destroy(ptr); - self.buf_dynamic = null; + + if (std.valgrind.runningOnValgrind() > 0) { + // Initialize our undefined fields so Valgrind can catch it. + // https://github.com/ziglang/zig/issues/19148 + self.buffer = undefined; } } + /// Make sure that we have an allocator. If we don't, set the state to + /// invalid so that any additional OSC data is discarded. + pub inline fn ensureAllocator(self: *Parser) bool { + if (self.alloc != null) return true; + log.warn("An allocator is required to process OSC {t} but none was provided.", .{self.state}); + self.state = .invalid; + return false; + } + + /// Set up a fixed Writer to collect the rest of the OSC data. + pub inline fn writeToFixed(self: *Parser) void { + self.fixed = .fixed(&self.buffer); + self.writer = &self.fixed.?; + } + + /// Set up an allocating Writer to collect the rest of the OSC data. If we + /// don't have an allocator or setting up the allocator fails, fall back to + /// writing to a fixed buffer and hope that it's big enough. + pub inline fn writeToAllocating(self: *Parser) void { + const alloc = self.alloc orelse { + // We don't have an allocator - fall back to a fixed buffer and hope + // that it's big enough. + self.writeToFixed(); + return; + }; + + self.allocating = std.Io.Writer.Allocating.initCapacity(alloc, 2048) catch { + // The allocator failed for some reason, fall back to a fixed buffer + // and hope that it's big enough. + self.writeToFixed(); + return; + }; + + self.writer = &self.allocating.?.writer; + } + /// Consume the next character c and advance the parser state. - pub fn next(self: *Parser, c: u8) void { - // If our buffer is full then we're invalid, so we set our state - // accordingly and indicate the sequence is incomplete so that we - // don't accidentally issue a command when ending. - // - // We always keep space for 1 byte at the end to null-terminate - // values. - if (self.buf_idx >= self.buf.len - 1) { - @branchHint(.cold); - if (self.state != .invalid) { - log.warn( - "OSC sequence too long (> {d}), ignoring. state={}", - .{ self.buf.len, self.state }, - ); - } + pub inline fn next(self: *Parser, c: u8) void { + // If the state becomes invalid for any reason, just discard + // any further input. + if (self.state == .invalid) return; - self.state = .invalid; - - // We have to do this here because it will never reach the - // switch statement below, since our buf_idx will always be - // too high after this. - self.complete = false; + // If a writer has been initialized, we just accumulate the rest of the + // OSC sequence in the writer's buffer and skip the state machine. + if (self.writer) |writer| { + writer.writeByte(c) catch |err| switch (err) { + // We have overflowed our buffer or had some other error, set the + // state to invalid so that we discard any further input. + error.WriteFailed => self.state = .invalid, + }; return; } - // We store everything in the buffer so we can do a better job - // logging if we get to an invalid command. - self.buf[self.buf_idx] = c; - self.buf_idx += 1; - - // log.warn("state = {} c = {x}", .{ self.state, c }); - switch (self.state) { - // If we get something during the invalid state, we've - // ruined our entry. - .invalid => self.complete = false, + // handled above, so should never be here + .invalid => unreachable, - .empty => switch (c) { + .start => switch (c) { '0' => self.state = .@"0", '1' => self.state = .@"1", '2' => self.state = .@"2", @@ -565,27 +518,13 @@ pub const Parser = struct { else => self.state = .invalid, }, - .swallow => {}, - .@"0" => switch (c) { - ';' => { - self.command = .{ .change_window_title = undefined }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_title }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, .@"1" => switch (c) { - ';' => { - self.command = .{ .change_window_icon = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_icon }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), '0' => self.state = .@"10", '1' => self.state = .@"11", '2' => self.state = .@"12", @@ -600,390 +539,162 @@ pub const Parser = struct { }, .@"10" => switch (c) { - ';' => osc_10: { - if (self.alloc == null) { - log.warn("OSC 10 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_10; - } - self.command = .{ .color_operation = .{ - .op = .osc_10, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '4' => { - self.state = .@"104"; - // If we have an allocator, then we can complete the OSC104 - if (self.alloc != null) self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), + '4' => self.state = .@"104", else => self.state = .invalid, }, .@"104" => switch (c) { - ';' => osc_104: { - if (self.alloc == null) { - log.warn("OSC 104 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_104; - } - self.command = .{ - .color_operation = .{ - .op = .osc_104, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"11" => switch (c) { - ';' => osc_11: { - if (self.alloc == null) { - log.warn("OSC 11 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_11; - } - self.command = .{ .color_operation = .{ - .op = .osc_11, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '0'...'9' => blk: { - if (self.alloc == null) { - log.warn("OSC 11{c} requires an allocator, but none was provided", .{c}); - self.state = .invalid; - break :blk; - } + ';' => if (self.ensureAllocator()) self.writeToFixed(), + '0' => self.state = .@"110", + '1' => self.state = .@"111", + '2' => self.state = .@"112", + '3' => self.state = .@"113", + '4' => self.state = .@"114", + '5' => self.state = .@"115", + '6' => self.state = .@"116", + '7' => self.state = .@"117", + '8' => self.state = .@"118", + '9' => self.state = .@"119", + else => self.state = .invalid, + }, - self.command = .{ - .color_operation = .{ - .op = switch (c) { - '0' => .osc_110, - '1' => .osc_111, - '2' => .osc_112, - '3' => .osc_113, - '4' => .osc_114, - '5' => .osc_115, - '6' => .osc_116, - '7' => .osc_117, - '8' => .osc_118, - '9' => .osc_119, - else => unreachable, - }, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + .@"110" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"111" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"112" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"113" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"114" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"115" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"116" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"117" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"118" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"119" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"12" => switch (c) { - ';' => osc_12: { - if (self.alloc == null) { - log.warn("OSC 12 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_12; - } - self.command = .{ .color_operation = .{ - .op = .osc_12, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"13" => switch (c) { - ';' => osc_13: { - if (self.alloc == null) { - log.warn("OSC 13 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_13; - } - self.command = .{ .color_operation = .{ - .op = .osc_13, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), '3' => self.state = .@"133", else => self.state = .invalid, }, .@"133" => switch (c) { - ';' => self.state = .semantic_prompt, + ';' => self.writeToFixed(), else => self.state = .invalid, }, .@"14" => switch (c) { - ';' => osc_14: { - if (self.alloc == null) { - log.warn("OSC 14 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_14; - } - self.command = .{ .color_operation = .{ - .op = .osc_14, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"15" => switch (c) { - ';' => osc_15: { - if (self.alloc == null) { - log.warn("OSC 15 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_15; - } - self.command = .{ .color_operation = .{ - .op = .osc_15, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"16" => switch (c) { - ';' => osc_16: { - if (self.alloc == null) { - log.warn("OSC 16 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_16; - } - self.command = .{ .color_operation = .{ - .op = .osc_16, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"17" => switch (c) { - ';' => osc_17: { - if (self.alloc == null) { - log.warn("OSC 17 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_17; - } - self.command = .{ .color_operation = .{ - .op = .osc_17, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"18" => switch (c) { - ';' => osc_18: { - if (self.alloc == null) { - log.warn("OSC 18 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_18; - } - self.command = .{ .color_operation = .{ - .op = .osc_18, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"19" => switch (c) { - ';' => osc_19: { - if (self.alloc == null) { - log.warn("OSC 19 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_19; - } - self.command = .{ .color_operation = .{ - .op = .osc_19, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, - .osc_color => {}, - .@"2" => switch (c) { + ';' => self.writeToFixed(), '1' => self.state = .@"21", '2' => self.state = .@"22", - ';' => { - self.command = .{ .change_window_title = undefined }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_title }; - self.buf_start = self.buf_idx; - }, else => self.state = .invalid, }, .@"21" => switch (c) { - ';' => kitty: { - if (self.alloc == null) { - log.info("OSC 21 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :kitty; - } - - self.command = .{ - .kitty_color_protocol = .{ - .list = .empty, - }, - }; - - self.temp_state = .{ .key = "" }; - self.state = .kitty_color_protocol_key; - self.complete = true; - self.buf_start = self.buf_idx; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, - .kitty_color_protocol_key => switch (c) { - ';' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.endKittyColorProtocolOption(.key_only, false); - self.state = .kitty_color_protocol_key; - self.buf_start = self.buf_idx; - }, - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .kitty_color_protocol_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .kitty_color_protocol_value => switch (c) { - ';' => { - self.endKittyColorProtocolOption(.key_and_value, false); - self.state = .kitty_color_protocol_key; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - .@"22" => switch (c) { - ';' => { - self.command = .{ .mouse_shape = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.mouse_shape.value }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, .@"4" => switch (c) { - ';' => osc_4: { - if (self.alloc == null) { - log.info("OSC 4 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_4; - } - self.command = .{ - .color_operation = .{ - .op = .osc_4, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"5" => switch (c) { - ';' => osc_5: { - if (self.alloc == null) { - log.info("OSC 5 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_5; - } - self.command = .{ - .color_operation = .{ - .op = .osc_5, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), '2' => self.state = .@"52", else => self.state = .invalid, }, .@"52" => switch (c) { - ';' => { - self.command = .{ .clipboard_contents = undefined }; - self.state = .clipboard_kind; - }, - else => self.state = .invalid, - }, - - .clipboard_kind => switch (c) { - ';' => { - self.command.clipboard_contents.kind = 'c'; - self.temp_state = .{ .str = &self.command.clipboard_contents.data }; - self.buf_start = self.buf_idx; - self.prepAllocableString(); - - // See clipboard_kind_end - self.complete = true; - }, - else => { - self.command.clipboard_contents.kind = c; - self.state = .clipboard_kind_end; - }, - }, - - .clipboard_kind_end => switch (c) { - ';' => { - self.temp_state = .{ .str = &self.command.clipboard_contents.data }; - self.buf_start = self.buf_idx; - self.prepAllocableString(); - - // OSC 52 can have empty payloads (quoting xterm ctlseqs): - // "If the second parameter is neither a base64 string nor ?, - // then the selection is cleared." - self.complete = true; - }, + ';' => self.writeToAllocating(), else => self.state = .invalid, }, .@"7" => switch (c) { - ';' => { - self.command = .{ .report_pwd = .{ .value = "" } }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.report_pwd.value }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), '7' => self.state = .@"77", else => self.state = .invalid, }, @@ -994,710 +705,22 @@ pub const Parser = struct { }, .@"777" => switch (c) { - ';' => { - self.state = .rxvt_extension; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, .@"8" => switch (c) { - ';' => { - self.command = .{ .hyperlink_start = .{ - .uri = "", - } }; - - self.state = .hyperlink_param_key; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, - .hyperlink_param_key => switch (c) { - ';' => { - self.complete = true; - self.state = .hyperlink_uri; - self.buf_start = self.buf_idx; - }, - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .hyperlink_param_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .hyperlink_param_value => switch (c) { - ':' => { - self.endHyperlinkOptionValue(); - self.state = .hyperlink_param_key; - self.buf_start = self.buf_idx; - }, - ';' => { - self.endHyperlinkOptionValue(); - self.state = .string; - self.temp_state = .{ .str = &self.command.hyperlink_start.uri }; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .hyperlink_uri => {}, - - .rxvt_extension => switch (c) { - 'a'...'z' => {}, - ';' => { - const ext = self.buf[self.buf_start .. self.buf_idx - 1]; - if (!std.mem.eql(u8, ext, "notify")) { - @branchHint(.cold); - log.warn("unknown rxvt extension: {s}", .{ext}); - self.state = .invalid; - return; - } - - self.command = .{ .show_desktop_notification = undefined }; - self.buf_start = self.buf_idx; - self.state = .notification_title; - }, - else => self.state = .invalid, - }, - - .notification_title => switch (c) { - ';' => { - self.buf[self.buf_idx - 1] = 0; - self.command.show_desktop_notification.title = self.buf[self.buf_start .. self.buf_idx - 1 :0]; - self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; - self.buf_start = self.buf_idx; - self.state = .string; - }, - else => {}, - }, - .@"9" => switch (c) { - ';' => { - self.buf_start = self.buf_idx; - self.state = .osc_9; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, - - .osc_9 => switch (c) { - '1' => { - self.state = .conemu_sleep; - // This will end up being either a ConEmu sleep OSC 9;1, - // or a desktop notification OSC 9 that begins with '1', so - // mark as complete. - self.complete = true; - }, - '2' => { - self.state = .conemu_message_box; - // This will end up being either a ConEmu message box OSC 9;2, - // or a desktop notification OSC 9 that begins with '2', so - // mark as complete. - self.complete = true; - }, - '3' => { - self.state = .conemu_tab; - // This will end up being either a ConEmu message box OSC 9;3, - // or a desktop notification OSC 9 that begins with '3', so - // mark as complete. - self.complete = true; - }, - '4' => { - self.state = .conemu_progress_prestate; - // This will end up being either a ConEmu progress report - // OSC 9;4, or a desktop notification OSC 9 that begins with - // '4', so mark as complete. - self.complete = true; - }, - '5' => { - // Note that sending an OSC 9 desktop notification that - // starts with 5 is impossible due to this. - self.state = .swallow; - self.command = .conemu_wait_input; - self.complete = true; - }, - '6' => { - self.state = .conemu_guimacro; - // This will end up being either a ConEmu GUI macro OSC 9;6, - // or a desktop notification OSC 9 that begins with '6', so - // mark as complete. - self.complete = true; - }, - - // Todo: parse out other ConEmu operating system commands. Even - // if we don't support them we probably don't want them showing - // up as desktop notifications. - - else => self.showDesktopNotification(), - }, - - .conemu_sleep => switch (c) { - ';' => { - self.command = .{ .conemu_sleep = .{ .duration_ms = 100 } }; - self.buf_start = self.buf_idx; - self.complete = true; - self.state = .conemu_sleep_value; - }, - - // OSC 9;1 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_sleep_value => switch (c) { - else => self.complete = true, - }, - - .conemu_message_box => switch (c) { - ';' => { - self.command = .{ .conemu_show_message_box = undefined }; - self.temp_state = .{ .str = &self.command.conemu_show_message_box }; - self.buf_start = self.buf_idx; - self.complete = true; - self.prepAllocableString(); - }, - - // OSC 9;2 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_tab => switch (c) { - ';' => { - self.state = .conemu_tab_txt; - self.command = .{ .conemu_change_tab_title = .reset }; - self.buf_start = self.buf_idx; - self.complete = true; - }, - - // OSC 9;3 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_tab_txt => { - self.command = .{ .conemu_change_tab_title = .{ .value = undefined } }; - self.temp_state = .{ .str = &self.command.conemu_change_tab_title.value }; - self.complete = true; - self.prepAllocableString(); - }, - - .conemu_progress_prestate => switch (c) { - ';' => { - self.command = .{ .conemu_progress_report = .{ - .state = undefined, - } }; - self.state = .conemu_progress_state; - }, - - // OSC 9;4 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_state => switch (c) { - '0' => { - self.command.conemu_progress_report.state = .remove; - self.state = .swallow; - self.complete = true; - }, - '1' => { - self.command.conemu_progress_report.state = .set; - self.command.conemu_progress_report.progress = 0; - self.state = .conemu_progress_prevalue; - }, - '2' => { - self.command.conemu_progress_report.state = .@"error"; - self.complete = true; - self.state = .conemu_progress_prevalue; - }, - '3' => { - self.command.conemu_progress_report.state = .indeterminate; - self.complete = true; - self.state = .swallow; - }, - '4' => { - self.command.conemu_progress_report.state = .pause; - self.complete = true; - self.state = .conemu_progress_prevalue; - }, - - // OSC 9;4; is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_prevalue => switch (c) { - ';' => { - self.state = .conemu_progress_value; - }, - - // OSC 9;4;<0-4> is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_value => switch (c) { - '0'...'9' => value: { - // No matter what substate we're in, a number indicates - // a completed ConEmu progress command. - self.complete = true; - - // If we aren't a set substate, then we don't care - // about the value. - const p = &self.command.conemu_progress_report; - switch (p.state) { - .remove, - .indeterminate, - => break :value, - .set, - .@"error", - .pause, - => {}, - } - - if (p.state == .set) - assert(p.progress != null) - else if (p.progress == null) - p.progress = 0; - - // If we're over 100% we're done. - if (p.progress.? >= 100) break :value; - - // If we're over 10 then any new digit forces us to - // be 100. - if (p.progress.? >= 10) - p.progress = 100 - else { - const d = std.fmt.charToDigit(c, 10) catch 0; - p.progress = @min(100, (p.progress.? * 10) + d); - } - }, - - else => { - self.state = .swallow; - self.complete = true; - }, - }, - - .conemu_guimacro => switch (c) { - ';' => { - self.command = .{ .conemu_guimacro = undefined }; - self.temp_state = .{ .str = &self.command.conemu_guimacro }; - self.buf_start = self.buf_idx; - self.state = .string; - self.complete = true; - }, - - // OSC 9;6 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .semantic_prompt => switch (c) { - 'A' => { - self.state = .semantic_option_start; - self.command = .{ .prompt_start = .{} }; - self.complete = true; - }, - - 'B' => { - self.state = .semantic_option_start; - self.command = .{ .prompt_end = {} }; - self.complete = true; - }, - - 'C' => { - self.state = .semantic_option_start; - self.command = .{ .end_of_input = .{} }; - self.complete = true; - }, - - 'D' => { - self.state = .semantic_exit_code_start; - self.command = .{ .end_of_command = .{} }; - self.complete = true; - }, - - else => self.state = .invalid, - }, - - .semantic_option_start => switch (c) { - ';' => { - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .semantic_option_key => switch (c) { - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .semantic_option_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .semantic_option_value => switch (c) { - ';' => { - self.endSemanticOptionValue(); - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .semantic_exit_code_start => switch (c) { - ';' => { - // No longer complete, if ';' shows up we expect some code. - self.complete = false; - self.state = .semantic_exit_code; - self.temp_state = .{ .num = 0 }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .semantic_exit_code => switch (c) { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { - self.complete = true; - - const idx = self.buf_idx - self.buf_start; - if (idx > 0) self.temp_state.num *|= 10; - self.temp_state.num +|= c - '0'; - }, - ';' => { - self.endSemanticExitCode(); - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .allocable_string => { - const alloc = self.alloc.?; - const list = self.buf_dynamic.?; - list.append(alloc, c) catch { - self.state = .invalid; - return; - }; - - // Never consume buffer space for allocable strings - self.buf_idx -= 1; - - // We can complete at any time - self.complete = true; - }, - - .string => self.complete = true, } } - fn showDesktopNotification(self: *Parser) void { - self.command = .{ .show_desktop_notification = .{ - .title = "", - .body = undefined, - } }; - - self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; - self.state = .string; - // Set as complete as we've already seen one character that should be - // part of the notification. If we wait for another character to set - // `complete` when the state is `.string` we won't be able to send any - // single character notifications. - self.complete = true; - } - - fn prepAllocableString(self: *Parser) void { - assert(self.buf_dynamic == null); - - // We need an allocator. If we don't have an allocator, we - // pretend we're just a fixed buffer string and hope we fit! - const alloc = self.alloc orelse { - self.state = .string; - return; - }; - - // Allocate our dynamic buffer - const list = alloc.create(std.ArrayListUnmanaged(u8)) catch { - self.state = .string; - return; - }; - list.* = .{}; - - self.buf_dynamic = list; - self.state = .allocable_string; - } - - fn endHyperlink(self: *Parser) void { - switch (self.command) { - .hyperlink_start => |*v| { - self.buf[self.buf_idx] = 0; - const value = self.buf[self.buf_start..self.buf_idx :0]; - if (v.id == null and value.len == 0) { - self.command = .{ .hyperlink_end = {} }; - return; - } - - v.uri = value; - }, - - else => unreachable, - } - } - - fn endHyperlinkOptionValue(self: *Parser) void { - const value: [:0]const u8 = if (self.buf_start == self.buf_idx) - "" - else buf: { - self.buf[self.buf_idx - 1] = 0; - break :buf self.buf[self.buf_start .. self.buf_idx - 1 :0]; - }; - - if (mem.eql(u8, self.temp_state.key, "id")) { - switch (self.command) { - .hyperlink_start => |*v| { - // We treat empty IDs as null ids so that we can - // auto-assign. - if (value.len > 0) v.id = value; - }, - else => {}, - } - } else log.info("unknown hyperlink option: {s}", .{self.temp_state.key}); - } - - fn endSemanticOptionValue(self: *Parser) void { - const value = value: { - self.buf[self.buf_idx] = 0; - defer self.buf_idx += 1; - break :value self.buf[self.buf_start..self.buf_idx :0]; - }; - - if (mem.eql(u8, self.temp_state.key, "aid")) { - switch (self.command) { - .prompt_start => |*v| v.aid = value, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "cmdline")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .end_of_input => |*v| v.cmdline = string_encoding.printfQDecode(value) catch null, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "cmdline_url")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .end_of_input => |*v| v.cmdline = string_encoding.urlPercentDecode(value) catch null, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "redraw")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .prompt_start => |*v| { - const valid = if (value.len == 1) valid: { - switch (value[0]) { - '0' => v.redraw = false, - '1' => v.redraw = true, - else => break :valid false, - } - - break :valid true; - } else false; - - if (!valid) { - log.info("OSC 133 A invalid redraw value: {s}", .{value}); - } - }, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "special_key")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .prompt_start => |*v| { - const valid = if (value.len == 1) valid: { - switch (value[0]) { - '0' => v.special_key = false, - '1' => v.special_key = true, - else => break :valid false, - } - - break :valid true; - } else false; - - if (!valid) { - log.info("OSC 133 A invalid special_key value: {s}", .{value}); - } - }, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "click_events")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .prompt_start => |*v| { - const valid = if (value.len == 1) valid: { - switch (value[0]) { - '0' => v.click_events = false, - '1' => v.click_events = true, - else => break :valid false, - } - - break :valid true; - } else false; - - if (!valid) { - log.info("OSC 133 A invalid click_events value: {s}", .{value}); - } - }, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "k")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - // 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. - switch (self.command) { - .prompt_start => |*v| if (value.len == 1) { - v.kind = switch (value[0]) { - 'c' => .continuation, - 's' => .secondary, - 'r' => .right, - 'i' => .primary, - else => .primary, - }; - }, - else => {}, - } - } else log.info("unknown semantic prompts option: {s}", .{self.temp_state.key}); - } - - fn endSemanticExitCode(self: *Parser) void { - switch (self.command) { - .end_of_command => |*v| v.exit_code = @truncate(self.temp_state.num), - else => {}, - } - } - - fn endString(self: *Parser) void { - self.buf[self.buf_idx] = 0; - defer self.buf_idx += 1; - self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx :0]; - } - - fn endConEmuSleepValue(self: *Parser) void { - switch (self.command) { - .conemu_sleep => |*v| v.duration_ms = value: { - const str = self.buf[self.buf_start..self.buf_idx]; - if (str.len == 0) break :value 100; - - if (std.fmt.parseUnsigned(u16, str, 10)) |num| { - break :value @min(num, 10_000); - } else |_| { - break :value 100; - } - }, - else => {}, - } - } - - fn endKittyColorProtocolOption(self: *Parser, kind: enum { key_only, key_and_value }, final: bool) void { - if (self.temp_state.key.len == 0) { - @branchHint(.cold); - log.warn("zero length key in kitty color protocol", .{}); - return; - } - - const key = kitty_color.Kind.parse(self.temp_state.key) orelse { - @branchHint(.cold); - log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); - return; - }; - - const value = value: { - if (self.buf_start == self.buf_idx) break :value ""; - if (final) break :value std.mem.trim(u8, self.buf[self.buf_start..self.buf_idx], " "); - break :value std.mem.trim(u8, self.buf[self.buf_start .. self.buf_idx - 1], " "); - }; - - switch (self.command) { - .kitty_color_protocol => |*v| { - // Cap our allocation amount for our list. - if (v.list.items.len >= @as(usize, kitty_color.Kind.max) * 2) { - @branchHint(.cold); - self.state = .invalid; - log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); - return; - } - - // Asserted when the command is set to kitty_color_protocol - // that we have an allocator. - const alloc = self.alloc.?; - - if (kind == .key_only or value.len == 0) { - v.list.append(alloc, .{ .reset = key }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - } else if (mem.eql(u8, "?", value)) { - v.list.append(alloc, .{ .query = key }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - } else { - v.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}); - return; - }, - }, - }, - }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - } - }, - else => {}, - } - } - - fn endOscColor(self: *Parser) void { - const alloc = self.alloc.?; - assert(self.command == .color_operation); - const data = self.buf[self.buf_start..self.buf_idx]; - self.command.color_operation.requests = osc_color.parse( - alloc, - self.command.color_operation.op, - data, - ) catch |err| list: { - log.info( - "failed to parse OSC color request err={} data={s}", - .{ err, data }, - ); - break :list .{}; - }; - } - - fn endAllocableString(self: *Parser) void { - const alloc = self.alloc.?; - const list = self.buf_dynamic.?; - list.append(alloc, 0) catch { - @branchHint(.cold); - log.warn("allocation failed on allocable string termination", .{}); - self.temp_state.str.* = ""; - return; - }; - - self.temp_state.str.* = list.items[0 .. list.items.len - 1 :0]; - } - /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. The optional terminator_ch /// is the final character in the OSC sequence. This is used to determine @@ -1706,64 +729,767 @@ pub const Parser = struct { /// The returned pointer is only valid until the next call to the parser. /// Callers should copy out any data they wish to retain across calls. pub fn end(self: *Parser, terminator_ch: ?u8) ?*Command { - if (!self.complete) { - if (comptime !builtin.is_test) log.warn( - "invalid OSC command: {s}", - .{self.buf[0..self.buf_idx]}, - ); + return switch (self.state) { + .start => null, + .invalid => null, + .@"0" => self.parseChangeWindowTitle(terminator_ch), + .@"1" => self.parseChangeWindowIcon(terminator_ch), + .@"2" => self.parseChangeWindowTitle(terminator_ch), + .@"4" => self.parseOscColor(terminator_ch), + .@"5" => self.parseOscColor(terminator_ch), + .@"7" => self.parseReportPwd(terminator_ch), + .@"8" => self.parseHyperlink(terminator_ch), + .@"9" => self.parseOsc9(terminator_ch), + .@"10" => self.parseOscColor(terminator_ch), + .@"11" => self.parseOscColor(terminator_ch), + .@"12" => self.parseOscColor(terminator_ch), + .@"13" => self.parseOscColor(terminator_ch), + .@"14" => self.parseOscColor(terminator_ch), + .@"15" => self.parseOscColor(terminator_ch), + .@"16" => self.parseOscColor(terminator_ch), + .@"17" => self.parseOscColor(terminator_ch), + .@"18" => self.parseOscColor(terminator_ch), + .@"19" => self.parseOscColor(terminator_ch), + .@"21" => self.parseKittyColorProtocol(terminator_ch), + .@"22" => self.parseMouseShape(terminator_ch), + .@"52" => self.parseClipboardOperation(terminator_ch), + .@"77" => null, + .@"104" => self.parseOscColor(terminator_ch), + .@"110" => self.parseOscColor(terminator_ch), + .@"111" => self.parseOscColor(terminator_ch), + .@"112" => self.parseOscColor(terminator_ch), + .@"113" => self.parseOscColor(terminator_ch), + .@"114" => self.parseOscColor(terminator_ch), + .@"115" => self.parseOscColor(terminator_ch), + .@"116" => self.parseOscColor(terminator_ch), + .@"117" => self.parseOscColor(terminator_ch), + .@"118" => self.parseOscColor(terminator_ch), + .@"119" => self.parseOscColor(terminator_ch), + .@"133" => self.parseSemanticPrompt(terminator_ch), + .@"777" => self.parseRxvtExtension(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; + } - // Other cleanup we may have to do depending on state. - switch (self.state) { - .allocable_string => self.endAllocableString(), - .semantic_exit_code => self.endSemanticExitCode(), - .semantic_option_value => self.endSemanticOptionValue(), - .hyperlink_uri => self.endHyperlink(), - .string => self.endString(), - .conemu_sleep_value => self.endConEmuSleepValue(), - .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), - .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), - .osc_color => self.endOscColor(), - - // 104 abruptly ended turns into a reset palette command. - .@"104" => { - self.command = .{ .color_operation = .{ - .op = .osc_104, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.endOscColor(); + /// 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}); + } }, - - // We received OSC 9;X ST, but nothing else, finish off as a - // desktop notification with "X" as the body. - .conemu_sleep, - .conemu_message_box, - .conemu_tab, - .conemu_progress_prestate, - .conemu_progress_state, - .conemu_guimacro, - => { - self.showDesktopNotification(); - self.endString(); + '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; + } - // A ConEmu progress report that has reached these states is - // complete, don't do anything to them. - .conemu_progress_prevalue, - .conemu_progress_value, - => {}, + const SemanticPromptKVIterator = struct { + index: usize, + string: []u8, - else => {}, + 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..], + }; } - switch (self.command) { - .kitty_color_protocol => |*c| c.terminator = .init(terminator_ch), - .color_operation => |*c| c.terminator = .init(terminator_ch), - else => {}, - } + 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; } }; @@ -1794,7 +1520,6 @@ test "OSC 0: longer than buffer" { for (input) |ch| p.next(ch); try testing.expect(p.end(null) == null); - try testing.expect(p.complete == false); } test "OSC 0: one shorter than buffer length" { @@ -1803,7 +1528,7 @@ test "OSC 0: one shorter than buffer length" { var p: Parser = .init(null); const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - prefix.len - 1); + const title = "a" ** (Parser.MAX_BUF - 1); const input = prefix ++ title; for (input) |ch| p.next(ch); @@ -1818,13 +1543,12 @@ test "OSC 0: exactly at buffer length" { var p: Parser = .init(null); const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - prefix.len); + 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); - try testing.expect(p.complete == false); } test "OSC 1: change_window_icon" { @@ -2731,7 +2455,7 @@ test "OSC 21: kitty color protocol reset after invalid" { p.reset(); - try testing.expectEqual(Parser.State.empty, p.state); + try testing.expectEqual(Parser.State.start, p.state); p.next('X'); try testing.expectEqual(Parser.State.invalid, p.state); @@ -2874,6 +2598,20 @@ test "OSC 133: prompt_start with single option" { 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; From 2805c1e405b8b3e0e96a468b1014107e1b51eac1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 7 Jan 2026 12:36:23 -0600 Subject: [PATCH 3/6] osc: collapse switch cases --- src/terminal/osc.zig | 209 ++++++++++++++----------------------------- 1 file changed, 67 insertions(+), 142 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index d81244b9f..571d123d8 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -518,11 +518,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"0" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - .@"1" => switch (c) { ';' => self.writeToFixed(), '0' => self.state = .@"10", @@ -564,57 +559,26 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"110" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"111" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"112" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"113" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"114" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"115" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"116" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"117" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"118" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"119" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"12" => switch (c) { + .@"4", + .@"12", + .@"14", + .@"15", + .@"16", + .@"17", + .@"18", + .@"19", + .@"21", + .@"110", + .@"111", + .@"112", + .@"113", + .@"114", + .@"115", + .@"116", + .@"117", + .@"118", + .@"119", + => switch (c) { ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, @@ -625,41 +589,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"133" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"14" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"15" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"16" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"17" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"18" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"19" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - .@"2" => switch (c) { ';' => self.writeToFixed(), '1' => self.state = .@"21", @@ -667,21 +596,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"21" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"22" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"4" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - .@"5" => switch (c) { ';' => if (self.ensureAllocator()) self.writeToFixed(), '2' => self.state = .@"52", @@ -704,17 +618,13 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"777" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"8" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"9" => switch (c) { + .@"0", + .@"133", + .@"22", + .@"777", + .@"8", + .@"9", + => switch (c) { ';' => self.writeToFixed(), else => self.state = .invalid, }, @@ -731,41 +641,56 @@ pub const Parser = struct { pub fn end(self: *Parser, terminator_ch: ?u8) ?*Command { return switch (self.state) { .start => null, + .invalid => null, - .@"0" => self.parseChangeWindowTitle(terminator_ch), + + .@"0", + .@"2", + => self.parseChangeWindowTitle(terminator_ch), + .@"1" => self.parseChangeWindowIcon(terminator_ch), - .@"2" => self.parseChangeWindowTitle(terminator_ch), - .@"4" => self.parseOscColor(terminator_ch), - .@"5" => self.parseOscColor(terminator_ch), + + .@"4", + .@"5", + .@"10", + .@"11", + .@"12", + .@"13", + .@"14", + .@"15", + .@"16", + .@"17", + .@"18", + .@"19", + .@"104", + .@"110", + .@"111", + .@"112", + .@"113", + .@"114", + .@"115", + .@"116", + .@"117", + .@"118", + .@"119", + => self.parseOscColor(terminator_ch), + .@"7" => self.parseReportPwd(terminator_ch), + .@"8" => self.parseHyperlink(terminator_ch), + .@"9" => self.parseOsc9(terminator_ch), - .@"10" => self.parseOscColor(terminator_ch), - .@"11" => self.parseOscColor(terminator_ch), - .@"12" => self.parseOscColor(terminator_ch), - .@"13" => self.parseOscColor(terminator_ch), - .@"14" => self.parseOscColor(terminator_ch), - .@"15" => self.parseOscColor(terminator_ch), - .@"16" => self.parseOscColor(terminator_ch), - .@"17" => self.parseOscColor(terminator_ch), - .@"18" => self.parseOscColor(terminator_ch), - .@"19" => self.parseOscColor(terminator_ch), + .@"21" => self.parseKittyColorProtocol(terminator_ch), + .@"22" => self.parseMouseShape(terminator_ch), + .@"52" => self.parseClipboardOperation(terminator_ch), + .@"77" => null, - .@"104" => self.parseOscColor(terminator_ch), - .@"110" => self.parseOscColor(terminator_ch), - .@"111" => self.parseOscColor(terminator_ch), - .@"112" => self.parseOscColor(terminator_ch), - .@"113" => self.parseOscColor(terminator_ch), - .@"114" => self.parseOscColor(terminator_ch), - .@"115" => self.parseOscColor(terminator_ch), - .@"116" => self.parseOscColor(terminator_ch), - .@"117" => self.parseOscColor(terminator_ch), - .@"118" => self.parseOscColor(terminator_ch), - .@"119" => self.parseOscColor(terminator_ch), + .@"133" => self.parseSemanticPrompt(terminator_ch), + .@"777" => self.parseRxvtExtension(terminator_ch), }; } From 0b9b17cbe0c14a7f4ce56840b5708af9563ebbea Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 8 Jan 2026 13:40:21 -0600 Subject: [PATCH 4/6] osc: remove pub from internal parser functions --- src/terminal/osc.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 571d123d8..dd31224e2 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -451,7 +451,7 @@ pub const Parser = struct { /// Make sure that we have an allocator. If we don't, set the state to /// invalid so that any additional OSC data is discarded. - pub inline fn ensureAllocator(self: *Parser) bool { + inline fn ensureAllocator(self: *Parser) bool { if (self.alloc != null) return true; log.warn("An allocator is required to process OSC {t} but none was provided.", .{self.state}); self.state = .invalid; @@ -459,7 +459,7 @@ pub const Parser = struct { } /// Set up a fixed Writer to collect the rest of the OSC data. - pub inline fn writeToFixed(self: *Parser) void { + inline fn writeToFixed(self: *Parser) void { self.fixed = .fixed(&self.buffer); self.writer = &self.fixed.?; } @@ -467,7 +467,7 @@ pub const Parser = struct { /// Set up an allocating Writer to collect the rest of the OSC data. If we /// don't have an allocator or setting up the allocator fails, fall back to /// writing to a fixed buffer and hope that it's big enough. - pub inline fn writeToAllocating(self: *Parser) void { + inline fn writeToAllocating(self: *Parser) void { const alloc = self.alloc orelse { // We don't have an allocator - fall back to a fixed buffer and hope // that it's big enough. From 6ee1b3998e55fe66584b968aab342b422d8bb502 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 8 Jan 2026 13:50:33 -0600 Subject: [PATCH 5/6] osc: no defaults on Parser fields --- src/terminal/osc.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index dd31224e2..64b409cdd 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -315,22 +315,22 @@ pub const Parser = struct { /// Optional allocator used to accept data longer than MAX_BUF. /// This only applies to some commands (e.g. OSC 52) that can /// reasonably exceed MAX_BUF. - alloc: ?Allocator = null, + alloc: ?Allocator, /// Current state of the parser. - state: State = .start, + state: State, /// Buffer for temporary storage of OSC data - buffer: [MAX_BUF]u8 = undefined, + buffer: [MAX_BUF]u8, /// Fixed writer for accumulating OSC data - fixed: ?std.Io.Writer = null, + fixed: ?std.Io.Writer, /// Allocating writer for accumulating OSC data - allocating: ?std.Io.Writer.Allocating = null, + allocating: ?std.Io.Writer.Allocating, /// Pointer to the active writer for accumulating OSC data - writer: ?*std.Io.Writer = null, + writer: ?*std.Io.Writer, /// The command that is the result of parsing. - command: Command = .invalid, + command: Command, pub const State = enum { start, From f180f1c9b8b5e4f1fca505ace5485b13762bef32 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 8 Jan 2026 14:12:16 -0600 Subject: [PATCH 6/6] osc: remove inline from Parser.next --- src/benchmark/OscParser.zig | 2 +- src/terminal/Parser.zig | 2 +- src/terminal/osc.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/benchmark/OscParser.zig b/src/benchmark/OscParser.zig index bd82a3534..d4b416de8 100644 --- a/src/benchmark/OscParser.zig +++ b/src/benchmark/OscParser.zig @@ -100,7 +100,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { error.ReadFailed => return error.BenchmarkFailed, }; - for (osc_buf[0..len]) |c| self.parser.next(c); + for (osc_buf[0..len]) |c| @call(.always_inline, Parser.next, .{ &self.parser, c }); std.mem.doNotOptimizeAway(self.parser.end(std.ascii.control_code.bel)); self.parser.reset(); } diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 980906e49..34a23787f 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -359,7 +359,7 @@ inline fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { break :param null; }, .osc_put => osc_put: { - self.osc_parser.next(c); + @call(.always_inline, osc.Parser.next, .{ &self.osc_parser, c }); break :osc_put null; }, .csi_dispatch => csi_dispatch: { diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 64b409cdd..56184df46 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -486,7 +486,7 @@ pub const Parser = struct { } /// Consume the next character c and advance the parser state. - pub inline fn next(self: *Parser, c: u8) void { + pub fn next(self: *Parser, c: u8) void { // If the state becomes invalid for any reason, just discard // any further input. if (self.state == .invalid) return;