From 1daba40247432a20f14de3fe99ede98bad199b66 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 19 Jan 2026 10:53:54 -0600 Subject: [PATCH] osc 133: handle bare keys with no '=' Fixes #10379 --- src/terminal/osc/parsers/semantic_prompt.zig | 114 ++++++++++++++----- 1 file changed, 85 insertions(+), 29 deletions(-) diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index ac7298267..652fe34da 100644 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -33,64 +33,69 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { return null; }; while (it.next()) |kv| { - if (std.mem.eql(u8, kv.key, "aid")) { + const key = kv.key orelse continue; + if (std.mem.eql(u8, key, "aid")) { parser.command.prompt_start.aid = kv.value; - } else if (std.mem.eql(u8, kv.key, "redraw")) redraw: { + } else if (std.mem.eql(u8, 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]) { + const value = kv.value orelse break :value null; + if (value.len != 1) break :value null; + switch (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}); + log.info("OSC 133 A: invalid redraw value: {?s}", .{kv.value}); break :redraw; }; - } else if (std.mem.eql(u8, kv.key, "special_key")) redraw: { + } else if (std.mem.eql(u8, 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]) { + const value = kv.value orelse break :value null; + if (value.len != 1) break :value null; + switch (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}); + 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: { + } else if (std.mem.eql(u8, 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]) { + const value = kv.value orelse break :value null; + if (value.len != 1) break :value null; + switch (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}); + log.info("OSC 133 A invalid click_events value: {?s}", .{kv.value}); break :redraw; }; - } else if (std.mem.eql(u8, kv.key, "k")) k: { + } else if (std.mem.eql(u8, 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]) { + const value = kv.value orelse break :k; + if (value.len != 1) break :k; + parser.command.prompt_start.kind = switch (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}); + } else log.info("OSC 133 A: unknown semantic prompt option: {?s}", .{kv.key}); } }, 'B' => prompt_end: { @@ -105,7 +110,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { return null; }; while (it.next()) |kv| { - log.info("OSC 133 B: unknown semantic prompt option: {s}", .{kv.key}); + log.info("OSC 133 B: unknown semantic prompt option: {?s}", .{kv.key}); } }, 'C' => end_of_input: { @@ -122,12 +127,13 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { 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; + const key = kv.key orelse continue; + if (std.mem.eql(u8, key, "cmdline")) { + parser.command.end_of_input.cmdline = if (kv.value) |value| string_encoding.printfQDecode(value) catch null else null; + } else if (std.mem.eql(u8, key, "cmdline_url")) { + parser.command.end_of_input.cmdline = if (kv.value) |value| string_encoding.urlPercentDecode(value) catch null else null; } else { - log.info("OSC 133 C: unknown semantic prompt option: {s}", .{kv.key}); + log.info("OSC 133 C: unknown semantic prompt option: {s}", .{key}); } } }, @@ -159,8 +165,8 @@ const SemanticPromptKVIterator = struct { string: []u8, pub const SemanticPromptKV = struct { - key: [:0]u8, - value: [:0]u8, + key: ?[:0]u8, + value: ?[:0]u8, }; pub fn init(writer: *std.Io.Writer) std.Io.Writer.Error!SemanticPromptKVIterator { @@ -186,17 +192,24 @@ const SemanticPromptKVIterator = struct { break :kv kv; }; - // If we have an empty item, we return an empty key and value. + // If we have an empty item, we return a null key and value. // // This allows for trailing semicolons, but also lets us parse // (or rather, ignore) empty fields; for example `a=b;;e=f`. if (kv.len < 1) return .{ - .key = kv, - .value = kv, + .key = null, + .value = null, }; const key = key: { - const index = std.mem.indexOfScalar(u8, kv, '=') orelse break :key kv; + const index = std.mem.indexOfScalar(u8, kv, '=') orelse { + // If there is no '=' return entire `kv` string as the key and + // a null value. + return .{ + .key = kv, + .value = null, + }; + }; kv[index] = 0; const key = kv[0..index :0]; break :key key; @@ -408,6 +421,36 @@ test "OSC 133: prompt_start with click_events empty" { try testing.expect(cmd.prompt_start.click_events == false); } +test "OSC 133: prompt_start with click_events bare key" { + 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: prompt_start with invalid bare key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;barekey"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.aid == null); + try testing.expectEqual(.primary, cmd.prompt_start.kind); + try testing.expect(cmd.prompt_start.redraw == true); + try testing.expect(cmd.prompt_start.special_key == false); + try testing.expect(cmd.prompt_start.click_events == false); +} + test "OSC 133: end_of_command no exit code" { const testing = std.testing; @@ -713,3 +756,16 @@ test "OSC 133: end_of_input with cmdline_url 9" { try testing.expect(cmd == .end_of_input); try testing.expect(cmd.end_of_input.cmdline == null); } + +test "OSC 133: end_of_input with bare key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;C;cmdline_url"; + 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); +}