From d040c935e2cd9d3277aef85caf67ea958c7928e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:01:20 -0800 Subject: [PATCH 01/17] terminal/osc: boilerplate new OSC 133 parsing --- src/terminal/osc.zig | 7 +- src/terminal/osc/parsers.zig | 15 +-- src/terminal/osc/parsers/semantic_prompt.zig | 3 +- src/terminal/osc/parsers/semantic_prompt2.zig | 92 +++++++++++++++++++ src/terminal/stream.zig | 3 + 5 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 src/terminal/osc/parsers/semantic_prompt2.zig diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 368da4afc..65a1b1121 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -41,6 +41,9 @@ pub const Command = union(Key) { /// in the log. change_window_icon: [:0]const u8, + /// Semantic prompt command: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md + semantic_prompt: parsers.semantic_prompt2.Command, + /// First do a fresh-line. Then start a new command, and enter prompt mode: /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a /// prompt string (as if followed by OSC 133;P;k=i\007). Note: I've noticed @@ -225,6 +228,7 @@ pub const Command = union(Key) { "invalid", "change_window_title", "change_window_icon", + "semantic_prompt", "prompt_start", "prompt_end", "end_of_input", @@ -469,6 +473,7 @@ pub const Parser = struct { .prompt_end, .prompt_start, .report_pwd, + .semantic_prompt, .show_desktop_notification, .kitty_text_sizing, => {}, @@ -751,7 +756,7 @@ pub const Parser = struct { .@"77" => null, - .@"133" => parsers.semantic_prompt.parse(self, terminator_ch), + .@"133" => parsers.semantic_prompt2.parse(self, terminator_ch), .@"777" => parsers.rxvt_extension.parse(self, terminator_ch), diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig index f3028ec79..d005bd4c0 100644 --- a/src/terminal/osc/parsers.zig +++ b/src/terminal/osc/parsers.zig @@ -13,19 +13,8 @@ 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"); +pub const semantic_prompt2 = @import("parsers/semantic_prompt2.zig"); test { - _ = change_window_icon; - _ = change_window_title; - _ = clipboard_operation; - _ = color; - _ = hyperlink; - _ = iterm2; - _ = kitty_color; - _ = kitty_text_sizing; - _ = mouse_shape; - _ = osc9; - _ = report_pwd; - _ = rxvt_extension; - _ = semantic_prompt; + std.testing.refAllDecls(@This()); } diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index 652fe34da..d7cfe7c35 100644 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -1,7 +1,6 @@ +//! https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md const std = @import("std"); - const string_encoding = @import("../../../os/string_encoding.zig"); - const Parser = @import("../../osc.zig").Parser; const Command = @import("../../osc.zig").Command; diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig new file mode 100644 index 000000000..954c101a1 --- /dev/null +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -0,0 +1,92 @@ +//! https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md +const std = @import("std"); +const Parser = @import("../../osc.zig").Parser; +const OSCCommand = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_semantic_prompt); + +pub const Command = union(enum) { + fresh_line, + fresh_line_new_prompt: Options, +}; + +pub const Options = struct { + aid: ?[:0]const u8, + cl: ?Click, + // TODO: more + + pub const init: Options = .{ + .aid = null, + .click = null, + }; +}; + +pub const Click = enum { + line, + multiple, + conservative_vertical, + smart_vertical, +}; + +/// Parse OSC 133, semantic prompts +pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 0) { + parser.state = .invalid; + return null; + } + + parser.command = command: { + parse: switch (data[0]) { + 'L' => { + if (data.len > 1) break :parse; + break :command .{ .semantic_prompt = .fresh_line }; + }, + + else => {}, + } + + // Any fallthroughs are invalid + parser.state = .invalid; + return null; + }; + + return &parser.command; +} + +test "OSC 133: fresh_line" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;L"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line); +} + +test "OSC 133: fresh_line extra contents" { + const testing = std.testing; + + // Random + { + var p: Parser = .init(null); + const input = "133;Lol"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); + } + + // Options + { + var p: Parser = .init(null); + const input = "133;L;aid=foo"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); + } +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 4e1398d8d..d12c59ef3 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -2003,6 +2003,9 @@ pub fn Stream(comptime Handler: type) type { // ref: https://github.com/qwerasd205/asciinema-stats switch (cmd) { + // TODO + .semantic_prompt => {}, + .change_window_title => |title| { @branchHint(.likely); if (!std.unicode.utf8ValidateSlice(title)) { From 65c56c7c77be738034cc21200a092c69ca4a4281 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:10:51 -0800 Subject: [PATCH 02/17] terminal/osc: add 'A' --- src/terminal/osc/parsers/semantic_prompt2.zig | 251 +++++++++++++++++- 1 file changed, 239 insertions(+), 12 deletions(-) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index 954c101a1..62833f31a 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -17,8 +17,22 @@ pub const Options = struct { pub const init: Options = .{ .aid = null, - .click = null, + .cl = null, }; + + pub fn parse(self: *Options, it: *KVIterator) void { + while (it.next()) |kv| { + const key = kv.key orelse continue; + if (std.mem.eql(u8, key, "aid")) { + self.aid = kv.value; + } else if (std.mem.eql(u8, key, "cl")) cl: { + const value = kv.value orelse break :cl; + self.cl = std.meta.stringToEnum(Click, value); + } else { + log.info("OSC 133: unknown semantic prompt option: {s}", .{key}); + } + } + } }; pub const Click = enum { @@ -40,23 +54,108 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { return null; } - parser.command = command: { - parse: switch (data[0]) { - 'L' => { - if (data.len > 1) break :parse; - break :command .{ .semantic_prompt = .fresh_line }; + // All valid cases terminate within this block. Any fallthroughs + // are invalid. This makes some of our parse logic a little less + // repetitive. + valid: { + switch (data[0]) { + 'A' => fresh_line: { + parser.command = .{ .semantic_prompt = .{ .fresh_line_new_prompt = .init } }; + if (data.len == 1) break :fresh_line; + if (data[1] != ';') break :valid; + var it = KVIterator.init(writer) catch break :valid; + parser.command.semantic_prompt.fresh_line_new_prompt.parse(&it); }, - else => {}, + 'L' => { + if (data.len > 1) break :valid; + parser.command = .{ .semantic_prompt = .fresh_line }; + }, + + else => break :valid, } - // Any fallthroughs are invalid - parser.state = .invalid; - return null; + return &parser.command; + } + // Any fallthroughs are invalid + parser.state = .invalid; + return null; +} + +const KVIterator = struct { + index: usize, + string: []u8, + + pub const KV = struct { + key: ?[:0]u8, + value: ?[:0]u8, + + pub const empty: KV = .{ + .key = null, + .value = null, + }; }; - return &parser.command; -} + pub fn init(writer: *std.Io.Writer) std.Io.Writer.Error!KVIterator { + // 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: *KVIterator) ?KV { + 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; + }; + + // 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 .empty; + + const key = key: { + 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; + break :key kv[0..index :0]; + }; + const value = kv[key.len + 1 .. :0]; + + return .{ + .key = key, + .value = value, + }; + } +}; test "OSC 133: fresh_line" { const testing = std.testing; @@ -90,3 +189,131 @@ test "OSC 133: fresh_line extra contents" { try testing.expect(p.end(null) == null); } } + +test "OSC 133: fresh_line_new_prompt" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.aid == null); + try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == null); +} + +test "OSC 133: fresh_line_new_prompt with aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=14"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expectEqualStrings("14", cmd.semantic_prompt.fresh_line_new_prompt.aid.?); +} + +test "OSC 133: fresh_line_new_prompt with '=' in aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=a=b"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expectEqualStrings("a=b", cmd.semantic_prompt.fresh_line_new_prompt.aid.?); +} + +test "OSC 133: fresh_line_new_prompt with cl=line" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;cl=line"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == .line); +} + +test "OSC 133: fresh_line_new_prompt with cl=multiple" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;cl=multiple"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == .multiple); +} + +test "OSC 133: fresh_line_new_prompt with invalid cl" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;cl=invalid"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == null); +} + +test "OSC 133: fresh_line_new_prompt with trailing ;" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); +} + +test "OSC 133: fresh_line_new_prompt with bare key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;barekey"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.aid == null); + try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == null); +} + +test "OSC 133: fresh_line_new_prompt with multiple options" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=foo;cl=line"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.fresh_line_new_prompt.aid.?); + try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == .line); +} From 7968358234ed74c4c3e1f02ebe70eb37159d7b46 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:23:30 -0800 Subject: [PATCH 03/17] terminal/osc: semantic prompt options --- src/terminal/osc/parsers/semantic_prompt2.zig | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index 62833f31a..e3cdb815f 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -13,11 +13,16 @@ pub const Command = union(enum) { pub const Options = struct { aid: ?[:0]const u8, cl: ?Click, - // TODO: more + prompt_kind: ?PromptKind, + exit_code: ?i32, + err: ?[:0]const u8, pub const init: Options = .{ .aid = null, .cl = null, + .prompt_kind = null, + .exit_code = null, + .err = null, }; pub fn parse(self: *Options, it: *KVIterator) void { @@ -28,6 +33,15 @@ pub const Options = struct { } else if (std.mem.eql(u8, key, "cl")) cl: { const value = kv.value orelse break :cl; self.cl = std.meta.stringToEnum(Click, value); + } else if (std.mem.eql(u8, key, "k")) k: { + const value = kv.value orelse break :k; + if (value.len != 1) break :k; + self.prompt_kind = .init(value[0]); + } else if (std.mem.eql(u8, key, "err")) { + self.err = kv.value; + } else if (key.len == 0) exit_code: { + const value = kv.value orelse break :exit_code; + self.exit_code = std.fmt.parseInt(i32, value, 10) catch break :exit_code; } else { log.info("OSC 133: unknown semantic prompt option: {s}", .{key}); } @@ -42,6 +56,23 @@ pub const Click = enum { smart_vertical, }; +pub const PromptKind = enum { + initial, + right, + continuation, + secondary, + + pub fn init(c: u8) ?PromptKind { + return switch (c) { + 'i' => .initial, + 'r' => .right, + 'c' => .continuation, + 's' => .secondary, + else => null, + }; + } +}; + /// Parse OSC 133, semantic prompts pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { const writer = parser.writer orelse { From 39c0f79b8da5f1b914fe450c200bacb448e2fc56 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:35:13 -0800 Subject: [PATCH 04/17] terminal/osc: semantic prompt 'P' --- src/terminal/osc/parsers/semantic_prompt2.zig | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index e3cdb815f..b46157692 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -8,6 +8,7 @@ const log = std.log.scoped(.osc_semantic_prompt); pub const Command = union(enum) { fresh_line, fresh_line_new_prompt: Options, + prompt_start: Options, }; pub const Options = struct { @@ -103,6 +104,14 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { parser.command = .{ .semantic_prompt = .fresh_line }; }, + 'P' => prompt_start: { + parser.command = .{ .semantic_prompt = .{ .prompt_start = .init } }; + if (data.len == 1) break :prompt_start; + if (data[1] != ';') break :valid; + var it = KVIterator.init(writer) catch break :valid; + parser.command.semantic_prompt.prompt_start.parse(&it); + }, + else => break :valid, } @@ -348,3 +357,96 @@ test "OSC 133: fresh_line_new_prompt with multiple options" { try testing.expectEqualStrings("foo", cmd.semantic_prompt.fresh_line_new_prompt.aid.?); try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == .line); } + +test "OSC 133: prompt_start" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .prompt_start); + try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == null); +} + +test "OSC 133: prompt_start with k=i" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=i"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .prompt_start); + try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == .initial); +} + +test "OSC 133: prompt_start with k=r" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=r"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .prompt_start); + try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == .right); +} + +test "OSC 133: prompt_start with k=c" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=c"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .prompt_start); + try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == .continuation); +} + +test "OSC 133: prompt_start with k=s" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=s"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .prompt_start); + try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == .secondary); +} + +test "OSC 133: prompt_start with invalid k" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=x"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .prompt_start); + try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == null); +} + +test "OSC 133: prompt_start extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Pextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} From 0d9216bb5a06ff33e5aadbeea27bd3c63567f732 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:37:34 -0800 Subject: [PATCH 05/17] terminal/osc: semantic prompt 'N' --- src/terminal/osc/parsers/semantic_prompt2.zig | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index b46157692..890fe714b 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -8,6 +8,7 @@ const log = std.log.scoped(.osc_semantic_prompt); pub const Command = union(enum) { fresh_line, fresh_line_new_prompt: Options, + new_command: Options, prompt_start: Options, }; @@ -104,6 +105,14 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { parser.command = .{ .semantic_prompt = .fresh_line }; }, + 'N' => new_command: { + parser.command = .{ .semantic_prompt = .{ .new_command = .init } }; + if (data.len == 1) break :new_command; + if (data[1] != ';') break :valid; + var it = KVIterator.init(writer) catch break :valid; + parser.command.semantic_prompt.new_command.parse(&it); + }, + 'P' => prompt_start: { parser.command = .{ .semantic_prompt = .{ .prompt_start = .init } }; if (data.len == 1) break :prompt_start; @@ -450,3 +459,70 @@ test "OSC 133: prompt_start extra contents" { for (input) |ch| p.next(ch); try testing.expect(p.end(null) == null); } + +test "OSC 133: new_command" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;N"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .new_command); + try testing.expect(cmd.semantic_prompt.new_command.aid == null); + try testing.expect(cmd.semantic_prompt.new_command.cl == null); +} + +test "OSC 133: new_command with aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;N;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .new_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.new_command.aid.?); +} + +test "OSC 133: new_command with cl=line" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;N;cl=line"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .new_command); + try testing.expect(cmd.semantic_prompt.new_command.cl == .line); +} + +test "OSC 133: new_command with multiple options" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;N;aid=foo;cl=line"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .new_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.new_command.aid.?); + try testing.expect(cmd.semantic_prompt.new_command.cl == .line); +} + +test "OSC 133: new_command extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Nextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} From fdc6a6b10a216af505f134cd7982bb83f228f124 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:39:32 -0800 Subject: [PATCH 06/17] terminal/osc: semantic prompt 'B' --- src/terminal/osc/parsers/semantic_prompt2.zig | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index 890fe714b..d2547abe4 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -10,6 +10,7 @@ pub const Command = union(enum) { fresh_line_new_prompt: Options, new_command: Options, prompt_start: Options, + end_prompt_start_input: Options, }; pub const Options = struct { @@ -100,6 +101,14 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { parser.command.semantic_prompt.fresh_line_new_prompt.parse(&it); }, + 'B' => end_prompt: { + parser.command = .{ .semantic_prompt = .{ .end_prompt_start_input = .init } }; + if (data.len == 1) break :end_prompt; + if (data[1] != ';') break :valid; + var it = KVIterator.init(writer) catch break :valid; + parser.command.semantic_prompt.end_prompt_start_input.parse(&it); + }, + 'L' => { if (data.len > 1) break :valid; parser.command = .{ .semantic_prompt = .fresh_line }; @@ -526,3 +535,38 @@ test "OSC 133: new_command extra contents" { for (input) |ch| p.next(ch); try testing.expect(p.end(null) == null); } + +test "OSC 133: end_prompt_start_input" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;B"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_prompt_start_input); +} + +test "OSC 133: end_prompt_start_input extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Bextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_prompt_start_input with options" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;B;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_prompt_start_input); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_prompt_start_input.aid.?); +} From 7421e78f1eacdd4a53feb8582f5eaeb11bc82b1b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:42:23 -0800 Subject: [PATCH 07/17] terminal/osc: semantic prompt 'I' --- src/terminal/osc/parsers/semantic_prompt2.zig | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index d2547abe4..7dfc01b3e 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -11,6 +11,7 @@ pub const Command = union(enum) { new_command: Options, prompt_start: Options, end_prompt_start_input: Options, + end_prompt_start_input_terminate_eol: Options, }; pub const Options = struct { @@ -109,6 +110,14 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { parser.command.semantic_prompt.end_prompt_start_input.parse(&it); }, + 'I' => end_prompt_line: { + parser.command = .{ .semantic_prompt = .{ .end_prompt_start_input_terminate_eol = .init } }; + if (data.len == 1) break :end_prompt_line; + if (data[1] != ';') break :valid; + var it = KVIterator.init(writer) catch break :valid; + parser.command.semantic_prompt.end_prompt_start_input_terminate_eol.parse(&it); + }, + 'L' => { if (data.len > 1) break :valid; parser.command = .{ .semantic_prompt = .fresh_line }; @@ -570,3 +579,38 @@ test "OSC 133: end_prompt_start_input with options" { try testing.expect(cmd.semantic_prompt == .end_prompt_start_input); try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_prompt_start_input.aid.?); } + +test "OSC 133: end_prompt_start_input_terminate_eol" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;I"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_prompt_start_input_terminate_eol); +} + +test "OSC 133: end_prompt_start_input_terminate_eol extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Iextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_prompt_start_input_terminate_eol with options" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;I;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_prompt_start_input_terminate_eol); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_prompt_start_input_terminate_eol.aid.?); +} From 9d1282eb956e459de66f8fe1fe06bbe78312e04e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:44:34 -0800 Subject: [PATCH 08/17] terminal/osc: semantic prompt 'C' --- src/terminal/osc/parsers/semantic_prompt2.zig | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index 7dfc01b3e..d25f5485e 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -12,6 +12,7 @@ pub const Command = union(enum) { prompt_start: Options, end_prompt_start_input: Options, end_prompt_start_input_terminate_eol: Options, + end_input_start_output: Options, }; pub const Options = struct { @@ -118,6 +119,14 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { parser.command.semantic_prompt.end_prompt_start_input_terminate_eol.parse(&it); }, + 'C' => end_input: { + parser.command = .{ .semantic_prompt = .{ .end_input_start_output = .init } }; + if (data.len == 1) break :end_input; + if (data[1] != ';') break :valid; + var it = KVIterator.init(writer) catch break :valid; + parser.command.semantic_prompt.end_input_start_output.parse(&it); + }, + 'L' => { if (data.len > 1) break :valid; parser.command = .{ .semantic_prompt = .fresh_line }; @@ -224,6 +233,43 @@ const KVIterator = struct { } }; +test "OSC 133: end_input_start_output" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;C"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_input_start_output); + try testing.expect(cmd.semantic_prompt.end_input_start_output.aid == null); + try testing.expect(cmd.semantic_prompt.end_input_start_output.cl == null); +} + +test "OSC 133: end_input_start_output extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Cextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_input_start_output with options" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;C;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_input_start_output); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_input_start_output.aid.?); +} + test "OSC 133: fresh_line" { const testing = std.testing; From a9e23c135f1ea023e827f2a15b33f74bbe224789 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:47:27 -0800 Subject: [PATCH 09/17] terminal/osc: semantic prompt 'D' --- src/terminal/osc/parsers/semantic_prompt2.zig | 88 ++++++++++++++++++- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index d25f5485e..58d4b1835 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -13,15 +13,20 @@ pub const Command = union(enum) { end_prompt_start_input: Options, end_prompt_start_input_terminate_eol: Options, end_input_start_output: Options, + end_command: Options, }; pub const Options = struct { aid: ?[:0]const u8, cl: ?Click, prompt_kind: ?PromptKind, - exit_code: ?i32, err: ?[:0]const u8, + // Not technically an option that can be set with k=v and only + // present currently with command 'D' but its easier to just + // parse it into our options. + exit_code: ?i32, + pub const init: Options = .{ .aid = null, .cl = null, @@ -44,9 +49,6 @@ pub const Options = struct { self.prompt_kind = .init(value[0]); } else if (std.mem.eql(u8, key, "err")) { self.err = kv.value; - } else if (key.len == 0) exit_code: { - const value = kv.value orelse break :exit_code; - self.exit_code = std.fmt.parseInt(i32, value, 10) catch break :exit_code; } else { log.info("OSC 133: unknown semantic prompt option: {s}", .{key}); } @@ -127,6 +129,30 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { parser.command.semantic_prompt.end_input_start_output.parse(&it); }, + 'D' => end_command: { + parser.command = .{ .semantic_prompt = .{ .end_command = .init } }; + if (data.len == 1) break :end_command; + if (data[1] != ';') break :valid; + var it = KVIterator.init(writer) catch break :valid; + + // If there are options, the first option MUST be the + // exit code. The specification appears to mandate this + // and disallow options without an exit code. + { + const first = it.next() orelse break :end_command; + if (first.value != null) break :end_command; + const key = first.key orelse break :end_command; + parser.command.semantic_prompt.end_command.exit_code = std.fmt.parseInt( + i32, + key, + 10, + ) catch null; + } + + // Parse the remaining options + parser.command.semantic_prompt.end_command.parse(&it); + }, + 'L' => { if (data.len > 1) break :valid; parser.command = .{ .semantic_prompt = .fresh_line }; @@ -660,3 +686,57 @@ test "OSC 133: end_prompt_start_input_terminate_eol with options" { try testing.expect(cmd.semantic_prompt == .end_prompt_start_input_terminate_eol); try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_prompt_start_input_terminate_eol.aid.?); } + +test "OSC 133: end_command" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;D"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_command); + try testing.expect(cmd.semantic_prompt.end_command.exit_code == null); + try testing.expect(cmd.semantic_prompt.end_command.aid == null); + try testing.expect(cmd.semantic_prompt.end_command.err == null); +} + +test "OSC 133: end_command extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Dextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_command with exit code 0" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;D;0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_command); + try testing.expect(cmd.semantic_prompt.end_command.exit_code == 0); +} + +test "OSC 133: end_command with exit code and aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;D;12;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt == .end_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_command.aid.?); + try testing.expect(cmd.semantic_prompt.end_command.exit_code == 12); +} From edafe8620388288b7af7b3042aca31916071e8d6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 14:03:44 -0800 Subject: [PATCH 10/17] terminal/osc: semantic prompt is a struct not tagged union --- src/terminal/osc/parsers/semantic_prompt2.zig | 202 ++++++++++-------- 1 file changed, 109 insertions(+), 93 deletions(-) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index 58d4b1835..b9752a9f4 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -5,15 +5,30 @@ const OSCCommand = @import("../../osc.zig").Command; const log = std.log.scoped(.osc_semantic_prompt); -pub const Command = union(enum) { - fresh_line, - fresh_line_new_prompt: Options, - new_command: Options, - prompt_start: Options, - end_prompt_start_input: Options, - end_prompt_start_input_terminate_eol: Options, - end_input_start_output: Options, - end_command: Options, +/// A single semantic prompt command. +/// +/// Technically according to the spec, not all commands have options +/// but it is easier to be "liberal in what we accept" here since +/// all except one do and the spec does also say to ignore unknown +/// options. So, I think this is a fair interpretation. +pub const Command = struct { + action: Action, + options: Options, + + pub const Action = enum { + fresh_line, // 'L' + fresh_line_new_prompt, // 'A' + new_command, // 'N' + prompt_start, // 'P' + end_prompt_start_input, // 'B' + end_prompt_start_input_terminate_eol, // 'I' + end_input_start_output, // 'C' + end_command, // 'D' + }; + + pub fn init(action: Action) Command { + return .{ .action = action, .options = .init }; + } }; pub const Options = struct { @@ -40,12 +55,12 @@ pub const Options = struct { const key = kv.key orelse continue; if (std.mem.eql(u8, key, "aid")) { self.aid = kv.value; - } else if (std.mem.eql(u8, key, "cl")) cl: { - const value = kv.value orelse break :cl; + } else if (std.mem.eql(u8, key, "cl")) { + const value = kv.value orelse continue; self.cl = std.meta.stringToEnum(Click, value); - } else if (std.mem.eql(u8, key, "k")) k: { - const value = kv.value orelse break :k; - if (value.len != 1) break :k; + } else if (std.mem.eql(u8, key, "k")) { + const value = kv.value orelse continue; + if (value.len != 1) continue; self.prompt_kind = .init(value[0]); } else if (std.mem.eql(u8, key, "err")) { self.err = kv.value; @@ -98,39 +113,39 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { valid: { switch (data[0]) { 'A' => fresh_line: { - parser.command = .{ .semantic_prompt = .{ .fresh_line_new_prompt = .init } }; + parser.command = .{ .semantic_prompt = .init(.fresh_line_new_prompt) }; if (data.len == 1) break :fresh_line; if (data[1] != ';') break :valid; var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.fresh_line_new_prompt.parse(&it); + parser.command.semantic_prompt.options.parse(&it); }, 'B' => end_prompt: { - parser.command = .{ .semantic_prompt = .{ .end_prompt_start_input = .init } }; + parser.command = .{ .semantic_prompt = .init(.end_prompt_start_input) }; if (data.len == 1) break :end_prompt; if (data[1] != ';') break :valid; var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.end_prompt_start_input.parse(&it); + parser.command.semantic_prompt.options.parse(&it); }, 'I' => end_prompt_line: { - parser.command = .{ .semantic_prompt = .{ .end_prompt_start_input_terminate_eol = .init } }; + parser.command = .{ .semantic_prompt = .init(.end_prompt_start_input_terminate_eol) }; if (data.len == 1) break :end_prompt_line; if (data[1] != ';') break :valid; var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.end_prompt_start_input_terminate_eol.parse(&it); + parser.command.semantic_prompt.options.parse(&it); }, 'C' => end_input: { - parser.command = .{ .semantic_prompt = .{ .end_input_start_output = .init } }; + parser.command = .{ .semantic_prompt = .init(.end_input_start_output) }; if (data.len == 1) break :end_input; if (data[1] != ';') break :valid; var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.end_input_start_output.parse(&it); + parser.command.semantic_prompt.options.parse(&it); }, 'D' => end_command: { - parser.command = .{ .semantic_prompt = .{ .end_command = .init } }; + parser.command = .{ .semantic_prompt = .init(.end_command) }; if (data.len == 1) break :end_command; if (data[1] != ';') break :valid; var it = KVIterator.init(writer) catch break :valid; @@ -142,7 +157,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { const first = it.next() orelse break :end_command; if (first.value != null) break :end_command; const key = first.key orelse break :end_command; - parser.command.semantic_prompt.end_command.exit_code = std.fmt.parseInt( + parser.command.semantic_prompt.options.exit_code = std.fmt.parseInt( i32, key, 10, @@ -150,28 +165,28 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { } // Parse the remaining options - parser.command.semantic_prompt.end_command.parse(&it); + parser.command.semantic_prompt.options.parse(&it); }, 'L' => { if (data.len > 1) break :valid; - parser.command = .{ .semantic_prompt = .fresh_line }; + parser.command = .{ .semantic_prompt = .init(.fresh_line) }; }, 'N' => new_command: { - parser.command = .{ .semantic_prompt = .{ .new_command = .init } }; + parser.command = .{ .semantic_prompt = .init(.new_command) }; if (data.len == 1) break :new_command; if (data[1] != ';') break :valid; var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.new_command.parse(&it); + parser.command.semantic_prompt.options.parse(&it); }, 'P' => prompt_start: { - parser.command = .{ .semantic_prompt = .{ .prompt_start = .init } }; + parser.command = .{ .semantic_prompt = .init(.prompt_start) }; if (data.len == 1) break :prompt_start; if (data[1] != ';') break :valid; var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.prompt_start.parse(&it); + parser.command.semantic_prompt.options.parse(&it); }, else => break :valid, @@ -179,6 +194,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { return &parser.command; } + // Any fallthroughs are invalid parser.state = .invalid; return null; @@ -269,9 +285,9 @@ test "OSC 133: end_input_start_output" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_input_start_output); - try testing.expect(cmd.semantic_prompt.end_input_start_output.aid == null); - try testing.expect(cmd.semantic_prompt.end_input_start_output.cl == null); + try testing.expect(cmd.semantic_prompt.action == .end_input_start_output); + try testing.expect(cmd.semantic_prompt.options.aid == null); + try testing.expect(cmd.semantic_prompt.options.cl == null); } test "OSC 133: end_input_start_output extra contents" { @@ -292,8 +308,8 @@ test "OSC 133: end_input_start_output with options" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_input_start_output); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_input_start_output.aid.?); + try testing.expect(cmd.semantic_prompt.action == .end_input_start_output); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); } test "OSC 133: fresh_line" { @@ -306,7 +322,7 @@ test "OSC 133: fresh_line" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line); + try testing.expect(cmd.semantic_prompt.action == .fresh_line); } test "OSC 133: fresh_line extra contents" { @@ -339,9 +355,9 @@ test "OSC 133: fresh_line_new_prompt" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.aid == null); - try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == null); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.aid == null); + try testing.expect(cmd.semantic_prompt.options.cl == null); } test "OSC 133: fresh_line_new_prompt with aid" { @@ -354,8 +370,8 @@ test "OSC 133: fresh_line_new_prompt with aid" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); - try testing.expectEqualStrings("14", cmd.semantic_prompt.fresh_line_new_prompt.aid.?); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expectEqualStrings("14", cmd.semantic_prompt.options.aid.?); } test "OSC 133: fresh_line_new_prompt with '=' in aid" { @@ -368,8 +384,8 @@ test "OSC 133: fresh_line_new_prompt with '=' in aid" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); - try testing.expectEqualStrings("a=b", cmd.semantic_prompt.fresh_line_new_prompt.aid.?); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expectEqualStrings("a=b", cmd.semantic_prompt.options.aid.?); } test "OSC 133: fresh_line_new_prompt with cl=line" { @@ -382,8 +398,8 @@ test "OSC 133: fresh_line_new_prompt with cl=line" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == .line); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.cl == .line); } test "OSC 133: fresh_line_new_prompt with cl=multiple" { @@ -396,8 +412,8 @@ test "OSC 133: fresh_line_new_prompt with cl=multiple" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == .multiple); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.cl == .multiple); } test "OSC 133: fresh_line_new_prompt with invalid cl" { @@ -410,8 +426,8 @@ test "OSC 133: fresh_line_new_prompt with invalid cl" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == null); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.cl == null); } test "OSC 133: fresh_line_new_prompt with trailing ;" { @@ -424,7 +440,7 @@ test "OSC 133: fresh_line_new_prompt with trailing ;" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); } test "OSC 133: fresh_line_new_prompt with bare key" { @@ -437,9 +453,9 @@ test "OSC 133: fresh_line_new_prompt with bare key" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.aid == null); - try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == null); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.aid == null); + try testing.expect(cmd.semantic_prompt.options.cl == null); } test "OSC 133: fresh_line_new_prompt with multiple options" { @@ -452,9 +468,9 @@ test "OSC 133: fresh_line_new_prompt with multiple options" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .fresh_line_new_prompt); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.fresh_line_new_prompt.aid.?); - try testing.expect(cmd.semantic_prompt.fresh_line_new_prompt.cl == .line); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); + try testing.expect(cmd.semantic_prompt.options.cl == .line); } test "OSC 133: prompt_start" { @@ -467,8 +483,8 @@ test "OSC 133: prompt_start" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .prompt_start); - try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == null); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.options.prompt_kind == null); } test "OSC 133: prompt_start with k=i" { @@ -481,8 +497,8 @@ test "OSC 133: prompt_start with k=i" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .prompt_start); - try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == .initial); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.options.prompt_kind == .initial); } test "OSC 133: prompt_start with k=r" { @@ -495,8 +511,8 @@ test "OSC 133: prompt_start with k=r" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .prompt_start); - try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == .right); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.options.prompt_kind == .right); } test "OSC 133: prompt_start with k=c" { @@ -509,8 +525,8 @@ test "OSC 133: prompt_start with k=c" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .prompt_start); - try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == .continuation); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.options.prompt_kind == .continuation); } test "OSC 133: prompt_start with k=s" { @@ -523,8 +539,8 @@ test "OSC 133: prompt_start with k=s" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .prompt_start); - try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == .secondary); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.options.prompt_kind == .secondary); } test "OSC 133: prompt_start with invalid k" { @@ -537,8 +553,8 @@ test "OSC 133: prompt_start with invalid k" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .prompt_start); - try testing.expect(cmd.semantic_prompt.prompt_start.prompt_kind == null); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.options.prompt_kind == null); } test "OSC 133: prompt_start extra contents" { @@ -560,9 +576,9 @@ test "OSC 133: new_command" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .new_command); - try testing.expect(cmd.semantic_prompt.new_command.aid == null); - try testing.expect(cmd.semantic_prompt.new_command.cl == null); + try testing.expect(cmd.semantic_prompt.action == .new_command); + try testing.expect(cmd.semantic_prompt.options.aid == null); + try testing.expect(cmd.semantic_prompt.options.cl == null); } test "OSC 133: new_command with aid" { @@ -575,8 +591,8 @@ test "OSC 133: new_command with aid" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .new_command); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.new_command.aid.?); + try testing.expect(cmd.semantic_prompt.action == .new_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); } test "OSC 133: new_command with cl=line" { @@ -589,8 +605,8 @@ test "OSC 133: new_command with cl=line" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .new_command); - try testing.expect(cmd.semantic_prompt.new_command.cl == .line); + try testing.expect(cmd.semantic_prompt.action == .new_command); + try testing.expect(cmd.semantic_prompt.options.cl == .line); } test "OSC 133: new_command with multiple options" { @@ -603,9 +619,9 @@ test "OSC 133: new_command with multiple options" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .new_command); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.new_command.aid.?); - try testing.expect(cmd.semantic_prompt.new_command.cl == .line); + try testing.expect(cmd.semantic_prompt.action == .new_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); + try testing.expect(cmd.semantic_prompt.options.cl == .line); } test "OSC 133: new_command extra contents" { @@ -627,7 +643,7 @@ test "OSC 133: end_prompt_start_input" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_prompt_start_input); + try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input); } test "OSC 133: end_prompt_start_input extra contents" { @@ -648,8 +664,8 @@ test "OSC 133: end_prompt_start_input with options" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_prompt_start_input); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_prompt_start_input.aid.?); + try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); } test "OSC 133: end_prompt_start_input_terminate_eol" { @@ -662,7 +678,7 @@ test "OSC 133: end_prompt_start_input_terminate_eol" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_prompt_start_input_terminate_eol); + try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input_terminate_eol); } test "OSC 133: end_prompt_start_input_terminate_eol extra contents" { @@ -683,8 +699,8 @@ test "OSC 133: end_prompt_start_input_terminate_eol with options" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_prompt_start_input_terminate_eol); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_prompt_start_input_terminate_eol.aid.?); + try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input_terminate_eol); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); } test "OSC 133: end_command" { @@ -697,10 +713,10 @@ test "OSC 133: end_command" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_command); - try testing.expect(cmd.semantic_prompt.end_command.exit_code == null); - try testing.expect(cmd.semantic_prompt.end_command.aid == null); - try testing.expect(cmd.semantic_prompt.end_command.err == null); + try testing.expect(cmd.semantic_prompt.action == .end_command); + try testing.expect(cmd.semantic_prompt.options.exit_code == null); + try testing.expect(cmd.semantic_prompt.options.aid == null); + try testing.expect(cmd.semantic_prompt.options.err == null); } test "OSC 133: end_command extra contents" { @@ -722,8 +738,8 @@ test "OSC 133: end_command with exit code 0" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_command); - try testing.expect(cmd.semantic_prompt.end_command.exit_code == 0); + try testing.expect(cmd.semantic_prompt.action == .end_command); + try testing.expect(cmd.semantic_prompt.options.exit_code == 0); } test "OSC 133: end_command with exit code and aid" { @@ -736,7 +752,7 @@ test "OSC 133: end_command with exit code and aid" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); - try testing.expect(cmd.semantic_prompt == .end_command); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.end_command.aid.?); - try testing.expect(cmd.semantic_prompt.end_command.exit_code == 12); + try testing.expect(cmd.semantic_prompt.action == .end_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); + try testing.expect(cmd.semantic_prompt.options.exit_code == 12); } From 9f2808ce4052d02df497e08a363473f7ec6fbac6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 14:09:24 -0800 Subject: [PATCH 11/17] terminal: stream handles new SemanticPrompt type --- src/terminal/osc.zig | 4 ++- src/terminal/stream.zig | 9 +++++-- src/terminal/stream_readonly.zig | 34 +++++++++++++++++++++++++ src/termio/stream_handler.zig | 43 ++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 65a1b1121..aec0b495d 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -42,7 +42,7 @@ pub const Command = union(Key) { change_window_icon: [:0]const u8, /// Semantic prompt command: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md - semantic_prompt: parsers.semantic_prompt2.Command, + semantic_prompt: SemanticPrompt, /// First do a fresh-line. Then start a new command, and enter prompt mode: /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a @@ -221,6 +221,8 @@ pub const Command = union(Key) { /// Kitty text sizing protocol (OSC 66) kitty_text_sizing: parsers.kitty_text_sizing.OSC, + pub const SemanticPrompt = parsers.semantic_prompt2.Command; + pub const Key = LibEnum( if (build_options.c_abi) .c else .zig, // NOTE: Order matters, see LibEnum documentation. diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index d12c59ef3..5d4a37c43 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -130,6 +130,7 @@ pub const Action = union(Key) { set_attribute: sgr.Attribute, kitty_color_report: kitty.color.OSC, color_operation: ColorOperation, + semantic_prompt: SemanticPrompt, pub const Key = lib.Enum( lib_target, @@ -231,6 +232,7 @@ pub const Action = union(Key) { "set_attribute", "kitty_color_report", "color_operation", + "semantic_prompt", }, ); @@ -448,6 +450,8 @@ pub const Action = union(Key) { return {}; } }; + + pub const SemanticPrompt = osc.Command.SemanticPrompt; }; /// Returns a type that can process a stream of tty control characters. @@ -2003,8 +2007,9 @@ pub fn Stream(comptime Handler: type) type { // ref: https://github.com/qwerasd205/asciinema-stats switch (cmd) { - // TODO - .semantic_prompt => {}, + .semantic_prompt => |sp| { + try self.handler.vt(.semantic_prompt, sp); + }, .change_window_title => |title| { @branchHint(.likely); diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index 90fcead93..86879c0d5 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -161,6 +161,7 @@ pub const Handler = struct { .prompt_end => self.terminal.markSemanticPrompt(.input), .end_of_input => self.terminal.markSemanticPrompt(.command), .end_of_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input, + .semantic_prompt => self.semanticPrompt(value), .mouse_shape => self.terminal.mouse_shape = value, .color_operation => try self.colorOperation(value.op, &value.requests), .kitty_color_report => try self.kittyColorOperation(value), @@ -216,6 +217,39 @@ pub const Handler = struct { } } + fn semanticPrompt( + self: *Handler, + cmd: Action.SemanticPrompt, + ) void { + switch (cmd.action) { + .fresh_line => { + if (self.terminal.screens.active.cursor.x != 0) { + self.terminal.carriageReturn(); + self.terminal.index() catch {}; + } + }, + .fresh_line_new_prompt => { + if (self.terminal.screens.active.cursor.x != 0) { + self.terminal.carriageReturn(); + self.terminal.index() catch {}; + } + self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt; + }, + .new_command => {}, + .prompt_start => { + const kind = cmd.options.prompt_kind orelse .initial; + switch (kind) { + .initial, .right => self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt, + .continuation, .secondary => self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation, + } + }, + .end_prompt_start_input => self.terminal.markSemanticPrompt(.input), + .end_prompt_start_input_terminate_eol => self.terminal.markSemanticPrompt(.input), + .end_input_start_output => self.terminal.markSemanticPrompt(.command), + .end_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input, + } + } + fn setMode(self: *Handler, mode: modes.Mode, enabled: bool) !void { // Set the mode on the terminal self.terminal.modes.set(mode, enabled); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 2a2b338a4..29ffefbda 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -325,6 +325,7 @@ pub const StreamHandler = struct { .prompt_start => self.promptStart(value.aid, value.redraw), .prompt_continuation => self.promptContinuation(value.aid), .end_of_command => self.endOfCommand(value.exit_code), + .semantic_prompt => self.semanticPrompt(value), .mouse_shape => try self.setMouseShape(value), .configure_charset => self.configureCharset(value.slot, value.charset), .set_attribute => { @@ -1094,6 +1095,48 @@ pub const StreamHandler = struct { self.surfaceMessageWriter(.{ .stop_command = exit_code }); } + fn semanticPrompt( + self: *StreamHandler, + cmd: Stream.Action.SemanticPrompt, + ) void { + switch (cmd.action) { + .fresh_line => { + if (self.terminal.screens.active.cursor.x != 0) { + self.terminal.carriageReturn(); + self.terminal.index() catch {}; + } + }, + .fresh_line_new_prompt => { + if (self.terminal.screens.active.cursor.x != 0) { + self.terminal.carriageReturn(); + self.terminal.index() catch {}; + } + self.promptStart(cmd.options.aid, false); + }, + .new_command => {}, + .prompt_start => { + const kind = cmd.options.prompt_kind orelse .initial; + switch (kind) { + .initial, .right => self.promptStart(cmd.options.aid, false), + .continuation, .secondary => self.promptContinuation(cmd.options.aid), + } + }, + .end_prompt_start_input => self.terminal.markSemanticPrompt(.input), + .end_prompt_start_input_terminate_eol => self.terminal.markSemanticPrompt(.input), + .end_input_start_output => { + self.terminal.markSemanticPrompt(.command); + self.surfaceMessageWriter(.start_command); + }, + .end_command => { + const exit_code: ?u8 = if (cmd.options.exit_code) |code| + if (code >= 0 and code <= 255) @intCast(code) else null + else + null; + self.surfaceMessageWriter(.{ .stop_command = exit_code }); + }, + } + } + fn reportPwd(self: *StreamHandler, url: []const u8) !void { // Special handling for the empty URL. We treat the empty URL // as resetting the pwd as if we never saw a pwd. I can't find any From 6ce45fb65a07007384e28501ecb7390923a99378 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 14:19:23 -0800 Subject: [PATCH 12/17] terminal/osc: semantic prompt redraw option from Kitty --- src/terminal/osc/parsers/semantic_prompt2.zig | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index b9752a9f4..5ab615713 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -37,6 +37,13 @@ pub const Options = struct { prompt_kind: ?PromptKind, err: ?[:0]const u8, + // 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. + redraw: bool, + // Not technically an option that can be set with k=v and only // present currently with command 'D' but its easier to just // parse it into our options. @@ -48,6 +55,7 @@ pub const Options = struct { .prompt_kind = null, .exit_code = null, .err = null, + .redraw = false, }; pub fn parse(self: *Options, it: *KVIterator) void { @@ -64,6 +72,14 @@ pub const Options = struct { self.prompt_kind = .init(value[0]); } else if (std.mem.eql(u8, key, "err")) { self.err = kv.value; + } else if (std.mem.eql(u8, key, "redraw")) redraw: { + const value = kv.value orelse break :redraw; + if (value.len != 1) break :redraw; + self.redraw = switch (value[0]) { + '0' => false, + '1' => true, + else => break :redraw, + }; } else { log.info("OSC 133: unknown semantic prompt option: {s}", .{key}); } @@ -473,6 +489,62 @@ test "OSC 133: fresh_line_new_prompt with multiple options" { try testing.expect(cmd.semantic_prompt.options.cl == .line); } +test "OSC 133: fresh_line_new_prompt default redraw" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.redraw == true); +} + +test "OSC 133: fresh_line_new_prompt with redraw=0" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;redraw=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.redraw == false); +} + +test "OSC 133: fresh_line_new_prompt with redraw=1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;redraw=1"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.redraw == true); +} + +test "OSC 133: fresh_line_new_prompt with invalid redraw" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;redraw=x"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.options.redraw == true); +} + test "OSC 133: prompt_start" { const testing = std.testing; From 389439b167a0c2ec79c37f9ace003d8f926a19e5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 14:25:46 -0800 Subject: [PATCH 13/17] terminal: handle semantic prompt same as old --- src/terminal/stream_readonly.zig | 35 ++++++++++++----------- src/termio/stream_handler.zig | 49 ++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index 86879c0d5..ba2aec6f9 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -222,31 +222,32 @@ pub const Handler = struct { cmd: Action.SemanticPrompt, ) void { switch (cmd.action) { - .fresh_line => { - if (self.terminal.screens.active.cursor.x != 0) { - self.terminal.carriageReturn(); - self.terminal.index() catch {}; - } - }, .fresh_line_new_prompt => { - if (self.terminal.screens.active.cursor.x != 0) { - self.terminal.carriageReturn(); - self.terminal.index() catch {}; - } - self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt; - }, - .new_command => {}, - .prompt_start => { const kind = cmd.options.prompt_kind orelse .initial; switch (kind) { - .initial, .right => self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt, - .continuation, .secondary => self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation, + .initial, .right => { + self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt; + self.terminal.flags.shell_redraws_prompt = cmd.options.redraw; + }, + .continuation, .secondary => { + self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation; + }, } }, + .end_prompt_start_input => self.terminal.markSemanticPrompt(.input), - .end_prompt_start_input_terminate_eol => self.terminal.markSemanticPrompt(.input), .end_input_start_output => self.terminal.markSemanticPrompt(.command), .end_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input, + + // All of these commands weren't previously handled by our + // semantic prompt code. I am PR-ing the parser separate from the + // handling so we just ignore these like we did before, even + // though we should handle them eventually. + .end_prompt_start_input_terminate_eol, + .fresh_line, + .new_command, + .prompt_start, + => {}, } } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 29ffefbda..1bf22ff55 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1100,40 +1100,45 @@ pub const StreamHandler = struct { cmd: Stream.Action.SemanticPrompt, ) void { switch (cmd.action) { - .fresh_line => { - if (self.terminal.screens.active.cursor.x != 0) { - self.terminal.carriageReturn(); - self.terminal.index() catch {}; - } - }, .fresh_line_new_prompt => { - if (self.terminal.screens.active.cursor.x != 0) { - self.terminal.carriageReturn(); - self.terminal.index() catch {}; - } - self.promptStart(cmd.options.aid, false); - }, - .new_command => {}, - .prompt_start => { const kind = cmd.options.prompt_kind orelse .initial; switch (kind) { - .initial, .right => self.promptStart(cmd.options.aid, false), - .continuation, .secondary => self.promptContinuation(cmd.options.aid), + .initial, .right => { + self.terminal.markSemanticPrompt(.prompt); + self.terminal.flags.shell_redraws_prompt = cmd.options.redraw; + }, + .continuation, .secondary => { + self.terminal.markSemanticPrompt(.prompt_continuation); + }, } }, + .end_prompt_start_input => self.terminal.markSemanticPrompt(.input), - .end_prompt_start_input_terminate_eol => self.terminal.markSemanticPrompt(.input), .end_input_start_output => { self.terminal.markSemanticPrompt(.command); self.surfaceMessageWriter(.start_command); }, .end_command => { - const exit_code: ?u8 = if (cmd.options.exit_code) |code| - if (code >= 0 and code <= 255) @intCast(code) else null - else - null; - self.surfaceMessageWriter(.{ .stop_command = exit_code }); + // The specification seems to not specify the type but + // other terminals accept 32-bits, but exit codes are really + // bytes, so we just do our best here. + const code: u8 = code: { + const raw: i32 = cmd.options.exit_code orelse 0; + break :code std.math.cast(u8, raw) orelse 1; + }; + + self.surfaceMessageWriter(.{ .stop_command = code }); }, + + // All of these commands weren't previously handled by our + // semantic prompt code. I am PR-ing the parser separate from the + // handling so we just ignore these like we did before, even + // though we should handle them eventually. + .end_prompt_start_input_terminate_eol, + .fresh_line, + .new_command, + .prompt_start, + => {}, } } From d23722dbd7a670665553a0b1e67376203e8485ab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 14:34:24 -0800 Subject: [PATCH 14/17] terminal: remove old semantic prompt handling --- src/terminal/osc.zig | 81 +- src/terminal/osc/parsers.zig | 3 +- src/terminal/osc/parsers/osc9.zig | 8 +- src/terminal/osc/parsers/semantic_prompt.zig | 770 ------------------- src/terminal/stream.zig | 81 +- src/terminal/stream_readonly.zig | 8 - src/termio/stream_handler.zig | 29 - 7 files changed, 9 insertions(+), 971 deletions(-) delete mode 100644 src/terminal/osc/parsers/semantic_prompt.zig diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index aec0b495d..b9061e2e9 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -44,75 +44,6 @@ pub const Command = union(Key) { /// Semantic prompt command: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md semantic_prompt: SemanticPrompt, - /// First do a fresh-line. Then start a new command, and enter prompt mode: - /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a - /// prompt string (as if followed by OSC 133;P;k=i\007). Note: I've noticed - /// not all shells will send the prompt end code. - prompt_start: struct { - /// "aid" is an optional "application identifier" that helps disambiguate - /// nested shell sessions. It can be anything but is usually a process ID. - aid: ?[:0]const u8 = null, - /// "kind" tells us which kind of semantic prompt sequence this is: - /// - primary: normal, left-aligned first-line prompt (initial, default) - /// - continuation: an editable continuation line - /// - secondary: a non-editable continuation line - /// - right: a right-aligned prompt that may need adjustment during reflow - kind: enum { primary, continuation, secondary, right } = .primary, - /// If true, the shell will not redraw the prompt on resize so don't erase it. - /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - redraw: bool = true, - /// Use a special key instead of arrow keys to move the cursor on - /// mouse click. Useful if arrow keys have side-effets like triggering - /// auto-complete. The shell integration script should bind the special - /// key as needed. - /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - special_key: bool = false, - /// If true, the shell is capable of handling mouse click events. - /// Ghostty will then send a click event to the shell when the user - /// clicks somewhere in the prompt. The shell can then move the cursor - /// to that position or perform some other appropriate action. If false, - /// Ghostty may generate a number of fake key events to move the cursor - /// which is not very robust. - /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - click_events: bool = false, - }, - - /// End of prompt and start of user input, terminated by a OSC "133;C" - /// or another prompt (OSC "133;P"). - prompt_end: void, - - /// The OSC "133;C" command can be used to explicitly end - /// the input area and begin the output area. However, some applications - /// don't provide a convenient way to emit that command. - /// That is why we also specify an implicit way to end the input area - /// at the end of the line. In the case of multiple input lines: If the - /// cursor is on a fresh (empty) line and we see either OSC "133;P" or - /// OSC "133;I" then this is the start of a continuation input line. - /// If we see anything else, it is the start of the output area (or end - /// of command). - end_of_input: struct { - /// The command line that the user entered. - /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - cmdline: ?[:0]const u8 = null, - }, - - /// End of current command. - /// - /// The exit-code need not be specified if there are no options, - /// or if the command was cancelled (no OSC "133;C"), such as by typing - /// an interrupt/cancel character (typically ctrl-C) during line-editing. - /// Otherwise, it must be an integer code, where 0 means the command - /// succeeded, and other values indicate failure. In additing to the - /// exit-code there may be an err= option, which non-legacy terminals - /// should give precedence to. The err=_value_ option is more general: - /// an empty string is success, and any non-empty value (which need not - /// be an integer) is an error code. So to indicate success both ways you - /// could send OSC "133;D;0;err=\007", though `OSC "133;D;0\007" is shorter. - end_of_command: struct { - exit_code: ?u8 = null, - // TODO: err option - }, - /// Set or get clipboard contents. If data is null, then the current /// clipboard contents are sent to the pty. If data is set, this /// contents is set on the clipboard. @@ -221,7 +152,7 @@ pub const Command = union(Key) { /// Kitty text sizing protocol (OSC 66) kitty_text_sizing: parsers.kitty_text_sizing.OSC, - pub const SemanticPrompt = parsers.semantic_prompt2.Command; + pub const SemanticPrompt = parsers.semantic_prompt.Command; pub const Key = LibEnum( if (build_options.c_abi) .c else .zig, @@ -231,10 +162,6 @@ pub const Command = union(Key) { "change_window_title", "change_window_icon", "semantic_prompt", - "prompt_start", - "prompt_end", - "end_of_input", - "end_of_command", "clipboard_contents", "report_pwd", "mouse_shape", @@ -466,14 +393,10 @@ pub const Parser = struct { .conemu_sleep, .conemu_wait_input, .conemu_xterm_emulation, - .end_of_command, - .end_of_input, .hyperlink_end, .hyperlink_start, .invalid, .mouse_shape, - .prompt_end, - .prompt_start, .report_pwd, .semantic_prompt, .show_desktop_notification, @@ -758,7 +681,7 @@ pub const Parser = struct { .@"77" => null, - .@"133" => parsers.semantic_prompt2.parse(self, terminator_ch), + .@"133" => parsers.semantic_prompt.parse(self, terminator_ch), .@"777" => parsers.rxvt_extension.parse(self, terminator_ch), diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig index d005bd4c0..5570b7702 100644 --- a/src/terminal/osc/parsers.zig +++ b/src/terminal/osc/parsers.zig @@ -12,8 +12,7 @@ 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"); -pub const semantic_prompt2 = @import("parsers/semantic_prompt2.zig"); +pub const semantic_prompt = @import("parsers/semantic_prompt2.zig"); test { std.testing.refAllDecls(@This()); diff --git a/src/terminal/osc/parsers/osc9.zig b/src/terminal/osc/parsers/osc9.zig index aba6f294a..f636813d9 100644 --- a/src/terminal/osc/parsers/osc9.zig +++ b/src/terminal/osc/parsers/osc9.zig @@ -98,9 +98,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { }, // OSC 9;12 mark prompt start '2' => { - parser.command = .{ - .prompt_start = .{}, - }; + parser.command = .{ .semantic_prompt = .init(.fresh_line_new_prompt) }; return &parser.command; }, else => break :conemu, @@ -1125,7 +1123,7 @@ test "OSC: 9;12: ConEmu mark prompt start 1" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .prompt_start); + try testing.expect(cmd == .semantic_prompt); } test "OSC: 9;12: ConEmu mark prompt start 2" { @@ -1138,5 +1136,5 @@ test "OSC: 9;12: ConEmu mark prompt start 2" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .prompt_start); + try testing.expect(cmd == .semantic_prompt); } diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig deleted file mode 100644 index d7cfe7c35..000000000 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ /dev/null @@ -1,770 +0,0 @@ -//! https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md -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| { - 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, 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: { - 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}); - break :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: { - 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}); - break :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: { - 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}); - break :redraw; - }; - } 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. - 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}); - } - }, - '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| { - 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}", .{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; - }; - - // 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 = null, - .value = null, - }; - - const key = key: { - 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; - }; - - 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 trailing ;" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); -} - -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: 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; - - 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 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); -} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 5d4a37c43..d0d2c1bb3 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -111,8 +111,6 @@ pub const Action = union(Key) { apc_start, apc_end, apc_put: u8, - prompt_end, - end_of_input, end_hyperlink, active_status_display: ansi.StatusDisplay, decaln, @@ -122,9 +120,6 @@ pub const Action = union(Key) { progress_report: osc.Command.ProgressReport, start_hyperlink: StartHyperlink, clipboard_contents: ClipboardContents, - prompt_start: PromptStart, - prompt_continuation: PromptContinuation, - end_of_command: EndOfCommand, mouse_shape: MouseShape, configure_charset: ConfigureCharset, set_attribute: sgr.Attribute, @@ -213,8 +208,6 @@ pub const Action = union(Key) { "apc_start", "apc_end", "apc_put", - "prompt_end", - "end_of_input", "end_hyperlink", "active_status_display", "decaln", @@ -224,9 +217,6 @@ pub const Action = union(Key) { "progress_report", "start_hyperlink", "clipboard_contents", - "prompt_start", - "prompt_continuation", - "end_of_command", "mouse_shape", "configure_charset", "set_attribute", @@ -393,47 +383,6 @@ pub const Action = union(Key) { } }; - pub const PromptStart = struct { - aid: ?[]const u8, - redraw: bool, - - pub const C = extern struct { - aid: lib.String, - redraw: bool, - }; - - pub fn cval(self: PromptStart) PromptStart.C { - return .{ - .aid = .init(self.aid orelse ""), - .redraw = self.redraw, - }; - } - }; - - pub const PromptContinuation = struct { - aid: ?[]const u8, - - pub const C = lib.String; - - pub fn cval(self: PromptContinuation) PromptContinuation.C { - return .init(self.aid orelse ""); - } - }; - - pub const EndOfCommand = struct { - exit_code: ?u8, - - pub const C = extern struct { - exit_code: i16, - }; - - pub fn cval(self: EndOfCommand) EndOfCommand.C { - return .{ - .exit_code = if (self.exit_code) |code| @intCast(code) else -1, - }; - } - }; - pub const ConfigureCharset = lib.Struct(lib_target, struct { slot: charsets.Slots, charset: charsets.Charset, @@ -1992,10 +1941,9 @@ pub fn Stream(comptime Handler: type) type { // 4. hyperlink_start // 5. report_pwd // 6. color_operation - // 7. prompt_start - // 8. prompt_end + // 7. semantic_prompt // - // Together, these 8 commands make up about 96% of all + // Together, these 7 commands make up about 96% of all // OSC commands encountered in real world scenarios. // // Additionally, within the prongs, unlikely branch @@ -2008,6 +1956,7 @@ pub fn Stream(comptime Handler: type) type { switch (cmd) { .semantic_prompt => |sp| { + @branchHint(.likely); try self.handler.vt(.semantic_prompt, sp); }, @@ -2034,30 +1983,6 @@ pub fn Stream(comptime Handler: type) type { }); }, - .prompt_start => |v| { - @branchHint(.likely); - switch (v.kind) { - .primary, .right => try self.handler.vt(.prompt_start, .{ - .aid = v.aid, - .redraw = v.redraw, - }), - .continuation, .secondary => try self.handler.vt(.prompt_continuation, .{ - .aid = v.aid, - }), - } - }, - - .prompt_end => { - @branchHint(.likely); - try self.handler.vt(.prompt_end, {}); - }, - - .end_of_input => try self.handler.vt(.end_of_input, {}), - - .end_of_command => |end| { - try self.handler.vt(.end_of_command, .{ .exit_code = end.exit_code }); - }, - .report_pwd => |v| { @branchHint(.likely); try self.handler.vt(.report_pwd, .{ .url = v.value }); diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index ba2aec6f9..57227a057 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -153,14 +153,6 @@ pub const Handler = struct { .full_reset => self.terminal.fullReset(), .start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id), .end_hyperlink => self.terminal.screens.active.endHyperlink(), - .prompt_start => { - self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt; - self.terminal.flags.shell_redraws_prompt = value.redraw; - }, - .prompt_continuation => self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation, - .prompt_end => self.terminal.markSemanticPrompt(.input), - .end_of_input => self.terminal.markSemanticPrompt(.command), - .end_of_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input, .semantic_prompt => self.semanticPrompt(value), .mouse_shape => self.terminal.mouse_shape = value, .color_operation => try self.colorOperation(value.op, &value.requests), diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 1bf22ff55..cfe68fd1c 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -311,8 +311,6 @@ pub const StreamHandler = struct { }, .kitty_color_report => try self.kittyColorReport(value), .color_operation => try self.colorOperation(value.op, &value.requests, value.terminator), - .prompt_end => try self.promptEnd(), - .end_of_input => try self.endOfInput(), .end_hyperlink => try self.endHyperlink(), .active_status_display => self.terminal.status_display = value, .decaln => try self.decaln(), @@ -322,9 +320,6 @@ pub const StreamHandler = struct { .progress_report => self.progressReport(value), .start_hyperlink => try self.startHyperlink(value.uri, value.id), .clipboard_contents => try self.clipboardContents(value.kind, value.data), - .prompt_start => self.promptStart(value.aid, value.redraw), - .prompt_continuation => self.promptContinuation(value.aid), - .end_of_command => self.endOfCommand(value.exit_code), .semantic_prompt => self.semanticPrompt(value), .mouse_shape => try self.setMouseShape(value), .configure_charset => self.configureCharset(value.slot, value.charset), @@ -1071,30 +1066,6 @@ pub const StreamHandler = struct { }); } - inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) void { - _ = aid; - self.terminal.markSemanticPrompt(.prompt); - self.terminal.flags.shell_redraws_prompt = redraw; - } - - inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) void { - _ = aid; - self.terminal.markSemanticPrompt(.prompt_continuation); - } - - pub inline fn promptEnd(self: *StreamHandler) !void { - self.terminal.markSemanticPrompt(.input); - } - - pub inline fn endOfInput(self: *StreamHandler) !void { - self.terminal.markSemanticPrompt(.command); - self.surfaceMessageWriter(.start_command); - } - - inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) void { - self.surfaceMessageWriter(.{ .stop_command = exit_code }); - } - fn semanticPrompt( self: *StreamHandler, cmd: Stream.Action.SemanticPrompt, From afea12116d6632681e7a27ba50297eb942b6ce73 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 14:37:41 -0800 Subject: [PATCH 15/17] terminal/osc: Kitty extensions to semantic prompt options --- src/terminal/osc/parsers/semantic_prompt2.zig | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt2.zig index 5ab615713..3d6e6a13f 100644 --- a/src/terminal/osc/parsers/semantic_prompt2.zig +++ b/src/terminal/osc/parsers/semantic_prompt2.zig @@ -44,6 +44,22 @@ pub const Options = struct { // redraw the prompt so we should attempt to resize it. redraw: bool, + // Use a special key instead of arrow keys to move the cursor on + // mouse click. Useful if arrow keys have side-effets like triggering + // auto-complete. The shell integration script should bind the special + // key as needed. + // See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + special_key: bool, + + // If true, the shell is capable of handling mouse click events. + // Ghostty will then send a click event to the shell when the user + // clicks somewhere in the prompt. The shell can then move the cursor + // to that position or perform some other appropriate action. If false, + // Ghostty may generate a number of fake key events to move the cursor + // which is not very robust. + // See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + click_events: bool, + // Not technically an option that can be set with k=v and only // present currently with command 'D' but its easier to just // parse it into our options. @@ -56,6 +72,8 @@ pub const Options = struct { .exit_code = null, .err = null, .redraw = false, + .special_key = false, + .click_events = false, }; pub fn parse(self: *Options, it: *KVIterator) void { @@ -80,6 +98,22 @@ pub const Options = struct { '1' => true, else => break :redraw, }; + } else if (std.mem.eql(u8, key, "special_key")) { + const value = kv.value orelse continue; + if (value.len != 1) continue; + self.special_key = switch (value[0]) { + '0' => false, + '1' => true, + else => continue, + }; + } else if (std.mem.eql(u8, key, "click_events")) { + const value = kv.value orelse continue; + if (value.len != 1) continue; + self.click_events = switch (value[0]) { + '0' => false, + '1' => true, + else => continue, + }; } else { log.info("OSC 133: unknown semantic prompt option: {s}", .{key}); } @@ -500,7 +534,7 @@ test "OSC 133: fresh_line_new_prompt default redraw" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.options.redraw == true); + try testing.expect(cmd.semantic_prompt.options.redraw == false); } test "OSC 133: fresh_line_new_prompt with redraw=0" { @@ -542,7 +576,7 @@ test "OSC 133: fresh_line_new_prompt with invalid redraw" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.options.redraw == true); + try testing.expect(cmd.semantic_prompt.options.redraw == false); } test "OSC 133: prompt_start" { From c98e3e6fc7e6793ad5ea883062f43210ce07ca0a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 14:38:28 -0800 Subject: [PATCH 16/17] terminal/osc: rename the prompt2 file --- src/terminal/osc/parsers.zig | 2 +- .../osc/parsers/{semantic_prompt2.zig => semantic_prompt.zig} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/terminal/osc/parsers/{semantic_prompt2.zig => semantic_prompt.zig} (100%) diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig index 5570b7702..fb84785f2 100644 --- a/src/terminal/osc/parsers.zig +++ b/src/terminal/osc/parsers.zig @@ -12,7 +12,7 @@ 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_prompt2.zig"); +pub const semantic_prompt = @import("parsers/semantic_prompt.zig"); test { std.testing.refAllDecls(@This()); diff --git a/src/terminal/osc/parsers/semantic_prompt2.zig b/src/terminal/osc/parsers/semantic_prompt.zig similarity index 100% rename from src/terminal/osc/parsers/semantic_prompt2.zig rename to src/terminal/osc/parsers/semantic_prompt.zig From 3f006f86a3421ee1ea1030ab28e65df99d9c1534 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 14:41:10 -0800 Subject: [PATCH 17/17] lib-vt: fix up the OSC command keys --- include/ghostty/vt/osc.h | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h index 7e2c8f322..f53077ab3 100644 --- a/include/ghostty/vt/osc.h +++ b/include/ghostty/vt/osc.h @@ -63,24 +63,26 @@ typedef enum { GHOSTTY_OSC_COMMAND_INVALID = 0, GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, - GHOSTTY_OSC_COMMAND_PROMPT_START = 3, - GHOSTTY_OSC_COMMAND_PROMPT_END = 4, - GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5, - GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6, - GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7, - GHOSTTY_OSC_COMMAND_REPORT_PWD = 8, - GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9, - GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10, - GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11, - GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12, - GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13, - GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14, - GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15, - GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16, - GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17, - GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18, - GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19, - GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20, + GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3, + GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4, + GHOSTTY_OSC_COMMAND_REPORT_PWD = 5, + GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6, + GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7, + GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8, + GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9, + GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10, + GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11, + GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12, + GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13, + GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14, + GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15, + GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16, + GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17, + GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18, + GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19, + GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20, + GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21, + GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22, } GhosttyOscCommandType; /**