From e1b82ff3981200fcd722734493b9ddc11ab82543 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 22 Jan 2026 22:06:39 -0600 Subject: [PATCH 01/20] osc: parse iTerm2 OSC 1337 extensions Add a framework for parsing iTerm2's OSC 1337 extensions. Implement a couple (`Copy` and `CurrentDir`) that map easily onto existing OSC commands. --- src/terminal/osc.zig | 17 +- src/terminal/osc/parsers.zig | 2 + src/terminal/osc/parsers/iterm2.zig | 435 ++++++++++++++++++++++++++++ 3 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 src/terminal/osc/parsers/iterm2.zig diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 3fccb2812..368da4afc 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -406,6 +406,7 @@ pub const Parser = struct { @"119", @"133", @"777", + @"1337", }; pub fn init(alloc: ?Allocator) Parser { @@ -663,8 +664,20 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"0", .@"133", + => switch (c) { + ';' => self.writeToFixed(), + '7' => self.state = .@"1337", + else => self.state = .invalid, + }, + + .@"1337", + => switch (c) { + ';' => self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"0", .@"22", .@"777", .@"8", @@ -741,6 +754,8 @@ pub const Parser = struct { .@"133" => parsers.semantic_prompt.parse(self, terminator_ch), .@"777" => parsers.rxvt_extension.parse(self, terminator_ch), + + .@"1337" => parsers.iterm2.parse(self, terminator_ch), }; } }; diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig index 9c1c39b2c..f3028ec79 100644 --- a/src/terminal/osc/parsers.zig +++ b/src/terminal/osc/parsers.zig @@ -5,6 +5,7 @@ pub const change_window_title = @import("parsers/change_window_title.zig"); pub const clipboard_operation = @import("parsers/clipboard_operation.zig"); pub const color = @import("parsers/color.zig"); pub const hyperlink = @import("parsers/hyperlink.zig"); +pub const iterm2 = @import("parsers/iterm2.zig"); pub const kitty_color = @import("parsers/kitty_color.zig"); pub const kitty_text_sizing = @import("parsers/kitty_text_sizing.zig"); pub const mouse_shape = @import("parsers/mouse_shape.zig"); @@ -19,6 +20,7 @@ test { _ = clipboard_operation; _ = color; _ = hyperlink; + _ = iterm2; _ = kitty_color; _ = kitty_text_sizing; _ = mouse_shape; diff --git a/src/terminal/osc/parsers/iterm2.zig b/src/terminal/osc/parsers/iterm2.zig new file mode 100644 index 000000000..bd64977cf --- /dev/null +++ b/src/terminal/osc/parsers/iterm2.zig @@ -0,0 +1,435 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; +const simd = @import("../../../simd/main.zig"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_iterm2); + +const Key = enum { + AddAnnotation, + AddHiddenAnnotation, + Block, + Button, + ClearCapturedOutput, + ClearScrollback, + Copy, + CopyToClipboard, + CurrentDir, + CursorShape, + Custom, + Disinter, + EndCopy, + File, + FileEnd, + FilePart, + HighlightCursorLine, + MultipartFile, + OpenURL, + PopKeyLabels, + PushKeyLabels, + RemoteHost, + ReportCellSize, + ReportVariable, + RequestAttention, + RequestUpload, + SetBackgroundImageFile, + SetBadgeFormat, + SetColors, + SetKeyLabel, + SetMark, + SetProfile, + SetUserVar, + ShellIntegrationVersion, + StealFocus, + UnicodeVersion, +}; + +// Instead of using `std.meta.stringToEnum` we set up a StaticStringMap so +// that we can get ASCII case-insensitive lookups. +const Map = std.StaticStringMapWithEql(Key, std.ascii.eqlIgnoreCase); +const map: Map = .initComptime( + map: { + const fields = @typeInfo(Key).@"enum".fields; + var tmp: [fields.len]struct { [:0]const u8, Key } = undefined; + for (fields, 0..) |field, i| { + tmp[i] = .{ field.name, @enumFromInt(field.value) }; + } + break :map tmp; + }, +); + +/// Parse OSC 1337 +/// https://iterm2.com/documentation-escape-codes.html +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + assert(parser.state == .@"1337"); + + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + + const key_str: [:0]u8, const value_: ?[:0]u8 = kv: { + const index = std.mem.indexOfScalar(u8, data, '=') orelse { + break :kv .{ data[0 .. data.len - 1 :0], null }; + }; + data[index] = 0; + break :kv .{ data[0..index :0], data[index + 1 .. data.len - 1 :0] }; + }; + + const key = map.get(key_str) orelse { + parser.command = .invalid; + return null; + }; + + switch (key) { + .Copy => { + var value = value_ orelse { + parser.command = .invalid; + return null; + }; + + // Sending a blank entry to clear the clipboard is an OSC 52-ism, + // make sure that is invalid here. + if (value.len == 0) { + parser.command = .invalid; + return null; + } + + // base64 value must be prefixed by a colon + if (value[0] != ':') { + parser.command = .invalid; + return null; + } + + value = value[1..value.len :0]; + + // Sending a blank entry to clear the clipboard is an OSC 52-ism, + // make sure that is invalid here. + if (value.len == 0) { + parser.command = .invalid; + return null; + } + + // Sending a '?' to query the clipboard is an OSC 52-ism, make sure + // that is invalid here. + if (value.len == 1 and value[0] == '?') { + parser.command = .invalid; + return null; + } + + // It would be better to check for valid base64 data here, but that + // would mean parsing the base64 data twice in the "normal" case. + + parser.command = .{ + .clipboard_contents = .{ + .kind = 'c', + .data = value, + }, + }; + return &parser.command; + }, + + .CurrentDir => { + const value = value_ orelse { + parser.command = .invalid; + return null; + }; + if (value.len == 0) { + parser.command = .invalid; + return null; + } + parser.command = .{ + .report_pwd = .{ + .value = value, + }, + }; + return &parser.command; + }, + + .AddAnnotation, + .AddHiddenAnnotation, + .Block, + .Button, + .ClearCapturedOutput, + .ClearScrollback, + .CopyToClipboard, + .CursorShape, + .Custom, + .Disinter, + .EndCopy, + .File, + .FileEnd, + .FilePart, + .HighlightCursorLine, + .MultipartFile, + .OpenURL, + .PopKeyLabels, + .PushKeyLabels, + .RemoteHost, + .ReportCellSize, + .ReportVariable, + .RequestAttention, + .RequestUpload, + .SetBackgroundImageFile, + .SetBadgeFormat, + .SetColors, + .SetKeyLabel, + .SetMark, + .SetProfile, + .SetUserVar, + .ShellIntegrationVersion, + .StealFocus, + .UnicodeVersion, + => { + log.debug("unimplemented OSC 1337: {t}", .{key}); + parser.command = .invalid; + return null; + }, + } + return &parser.command; +} + +test "OSC: 1337: test valid unimplemented key with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;SetBadgeFormat"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid unimplemented key with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;SetBadgeFormat="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid unimplemented key with non-empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;SetBadgeFormat=abc123"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid key with lower case and with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;setbadgeformat"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid key with lower case and with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;setbadgeformat="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid key with lower case and with non-empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;setbadgeformat=abc123"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test invalid key with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;BobrKurwa"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test invalid key with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;BobrKurwa="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test invalid key with non-empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;BobrKurwa=abc123"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with only prefix colon" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy=:"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with question mark" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy=:?"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with non-empty value that is invalid base64" { + // For performance reasons, we don't check for valid base64 data + // right now. + return error.SkipZigTest; + + // const testing = std.testing; + + // var p: Parser = .init(testing.allocator); + // defer p.deinit(); + + // const input = "1337;Copy=:abc123"; + // for (input) |ch| p.next(ch); + + // try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with non-empty value that is valid base64 but not prefixed with a colon" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy=YWJjMTIz"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with non-empty value that is valid base64" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy=:YWJjMTIz"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expectEqual('c', cmd.clipboard_contents.kind); + try testing.expectEqualStrings("YWJjMTIz", cmd.clipboard_contents.data); +} + +test "OSC: 1337: test CurrentDir with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;CurrentDir"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test CurrentDir with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;CurrentDir="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test CurrentDir with non-empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;CurrentDir=abc123"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .report_pwd); + try testing.expectEqualStrings("abc123", cmd.report_pwd.value); +} From f0b4e86ab55164347027eff262a9f932d4e62214 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 Jan 2026 12:09:18 -0600 Subject: [PATCH 02/20] gtk: add read-only indicator for surfaces Fixes: #9889 --- src/apprt/gtk/class/application.zig | 11 +++++++++- src/apprt/gtk/class/surface.zig | 34 +++++++++++++++++++++++++++++ src/apprt/gtk/css/style.css | 10 +++++++++ src/apprt/gtk/ui/1.2/surface.blp | 33 ++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 6a07cab84..403f94599 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -733,6 +733,7 @@ pub const Application = extern struct { .toggle_split_zoom => return Action.toggleSplitZoom(target), .show_on_screen_keyboard => return Action.showOnScreenKeyboard(target), .command_finished => return Action.commandFinished(target, value), + .readonly => return Action.setReadonly(target, value), .start_search => Action.startSearch(target, value), .end_search => Action.endSearch(target), @@ -753,7 +754,6 @@ pub const Application = extern struct { .check_for_updates, .undo, .redo, - .readonly, => { log.warn("unimplemented action={}", .{action}); return false; @@ -2678,6 +2678,15 @@ const Action = struct { } } + pub fn setReadonly(target: apprt.Target, value: apprt.Action.Value(.readonly)) bool { + switch (target) { + .app => return false, + .surface => |surface| { + return surface.rt_surface.gobj().setReadonly(value); + }, + } + } + pub fn keySequence(target: apprt.Target, value: apprt.Action.Value(.key_sequence)) bool { switch (target) { .app => { diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 5c3bf18b6..7a1aa4326 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -400,6 +400,25 @@ pub const Surface = extern struct { }, ); }; + + pub const readonly = struct { + pub const name = "readonly"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ + .getter = getReadonly, + }, + ), + }, + ); + }; }; pub const signals = struct { @@ -1106,6 +1125,20 @@ pub const Surface = extern struct { return true; } + /// Get the readonly state from the core surface. + pub fn getReadonly(self: *Self) bool { + const priv: *Private = self.private(); + const surface = priv.core_surface orelse return false; + return surface.readonly; + } + + /// Notify anyone interested that the readonly status has changed. + pub fn setReadonly(self: *Self, _: apprt.Action.Value(.readonly)) bool { + self.as(gobject.Object).notifyByPspec(properties.readonly.impl.param_spec); + + return true; + } + /// Key press event (press or release). /// /// At a high level, we want to construct an `input.KeyEvent` and @@ -3480,6 +3513,7 @@ pub const Surface = extern struct { properties.@"title-override".impl, properties.zoom.impl, properties.@"is-split".impl, + properties.readonly.impl, // For Gtk.Scrollable properties.hadjustment.impl, diff --git a/src/apprt/gtk/css/style.css b/src/apprt/gtk/css/style.css index f5491b7de..9c0f115f1 100644 --- a/src/apprt/gtk/css/style.css +++ b/src/apprt/gtk/css/style.css @@ -134,6 +134,16 @@ label.resize-overlay { border-style: solid; } +.surface .readonly_overlay { + /* Should be the equivalent of the following SwiftUI color: */ + /* Color(hue: 0.08, saturation: 0.5, brightness: 0.8) */ + color: hsl(25 50 75); + padding: 8px 8px 8px 8px; + margin: 8px 8px 8px 8px; + border-radius: 6px 6px 6px 6px; + outline-style: solid; + outline-width: 1px; +} /* * Command Palette */ diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index a594ba98f..dd6ded5de 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -71,6 +71,39 @@ Overlay terminal_page { } }; + [overlay] + Revealer { + reveal-child: bind template.readonly; + transition-type: crossfade; + transition-duration: 500; + // Revealers take up the full size, we need this to not capture events. + can-focus: false; + can-target: false; + focusable: false; + + Box readonly_overlay { + styles [ + "readonly_overlay", + ] + + // TODO: the tooltip doesn't actually work, but keep it here for now so + // that we can get the tooltip text translated. + has-tooltip: true; + tooltip-text: _("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application."); + halign: end; + valign: start; + spacing: 6; + + Image { + icon-name: "changes-prevent-symbolic"; + } + + Label { + label: _("Read-only"); + } + } + } + [overlay] ProgressBar progress_bar_overlay { styles [ From d040c935e2cd9d3277aef85caf67ea958c7928e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Jan 2026 13:01:20 -0800 Subject: [PATCH 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 08/20] 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 09/20] 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 10/20] 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 11/20] 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 12/20] 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 13/20] 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 14/20] 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 15/20] 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 16/20] 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 17/20] 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 18/20] 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 19/20] 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; /** From c9e60b322b61fcb2c161f69311de1cbab5ea58f8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 24 Jan 2026 13:30:06 -0800 Subject: [PATCH 20/20] terminal: OSC133 options parse from raw string This changes our OSC133 parser to parse options lazily. We do this for multiple reasons: 1. Parsing all our options ahead of time balloons our required osc.Command tagged union type which has C ABI implications. Adding all supported options (including Kitty extensions) today already breaks our C ABI. 2. Invalid options are allowed by the specification and should be explicitly ignored, so we don't need to validate options at all during parse time. 3. Semantic prompt markers don't need to be high throughput, so we can afford to do some extra work at processing time to gather the options. They're also rather short usually. --- src/terminal/osc/parsers/semantic_prompt.zig | 447 ++++++++++--------- src/terminal/stream_readonly.zig | 6 +- src/termio/stream_handler.zig | 8 +- 3 files changed, 251 insertions(+), 210 deletions(-) diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index 3d6e6a13f..f6a0cb593 100644 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -13,7 +13,7 @@ const log = std.log.scoped(.osc_semantic_prompt); /// options. So, I think this is a fair interpretation. pub const Command = struct { action: Action, - options: Options, + options_unvalidated: []const u8, pub const Action = enum { fresh_line, // 'L' @@ -27,29 +27,39 @@ pub const Command = struct { }; pub fn init(action: Action) Command { - return .{ .action = action, .options = .init }; + return .{ + .action = action, + .options_unvalidated = "", + }; + } + + /// Read an option for this command. Returns null if unset or invalid. + pub fn readOption( + self: Command, + comptime option: Option, + ) ?option.Type() { + return option.read(self.options_unvalidated); } }; -pub const Options = struct { - aid: ?[:0]const u8, - cl: ?Click, - prompt_kind: ?PromptKind, - err: ?[:0]const u8, - +pub const Option = enum { + aid, + cl, + prompt_kind, + err, // 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, + redraw, // Use a special key instead of arrow keys to move the cursor on // mouse click. Useful if arrow keys have side-effets like triggering // auto-complete. The shell integration script should bind the special // key as needed. // See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - special_key: bool, + special_key, // If true, the shell is capable of handling mouse click events. // Ghostty will then send a click event to the shell when the user @@ -58,66 +68,119 @@ pub const Options = struct { // 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, + click_events, // Not technically an option that can be set with k=v and only // present currently with command 'D' but its easier to just // parse it into our options. - exit_code: ?i32, + exit_code, - pub const init: Options = .{ - .aid = null, - .cl = null, - .prompt_kind = null, - .exit_code = null, - .err = null, - .redraw = false, - .special_key = false, - .click_events = false, - }; + pub fn Type(comptime self: Option) type { + return switch (self) { + .aid => []const u8, + .cl => Click, + .prompt_kind => PromptKind, + .err => []const u8, + .redraw => bool, + .special_key => bool, + .click_events => bool, + .exit_code => i32, + }; + } - 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")) { - const value = kv.value orelse continue; - self.cl = std.meta.stringToEnum(Click, value); - } 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; - } 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 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}); + fn key(comptime self: Option) []const u8 { + return switch (self) { + .aid => "aid", + .cl => "cl", + .prompt_kind => "k", + .err => "err", + .redraw => "redraw", + .special_key => "special_key", + .click_events => "click_events", + + // special case, handled before ever calling key + .exit_code => unreachable, + }; + } + + /// Read the option value from the raw options string. + /// + /// The raw options string is the raw unparsed data after the + /// OSC 133 command. e.g. for `133;A;aid=14;cl=line`, the + /// raw options string would be `aid=14;cl=line`. + /// + /// Any errors in the raw string will return null since the OSC133 + /// specification says to ignore unknown or malformed options. + pub fn read( + comptime self: Option, + raw: []const u8, + ) ?self.Type() { + var remaining = raw; + while (remaining.len > 0) { + // Length of the next value is up to the `;` or the + // end of the string. + const len = std.mem.indexOfScalar( + u8, + remaining, + ';', + ) orelse remaining.len; + + // Grab our full value and move our cursor past the `;` + const full = remaining[0..len]; + + // If we're looking for exit_code we special case it. + // as the first value. + if (comptime self == .exit_code) { + return std.fmt.parseInt( + i32, + full, + 10, + ) catch null; } + + // Parse our key=value and verify our key matches our + // expectation. + const value = value: { + if (std.mem.indexOfScalar( + u8, + full, + '=', + )) |eql_idx| { + if (std.mem.eql( + u8, + full[0..eql_idx], + self.key(), + )) { + break :value full[eql_idx + 1 ..]; + } + } + + // No match! + if (len < remaining.len) { + remaining = remaining[len + 1 ..]; + continue; + } + + break; + }; + + return switch (self) { + .aid => value, + .cl => std.meta.stringToEnum(Click, value), + .prompt_kind => if (value.len == 1) PromptKind.init(value[0]) else null, + .err => value, + .redraw, .special_key, .click_events => if (value.len == 1) switch (value[0]) { + '0' => false, + '1' => true, + else => null, + } else null, + // Handled above + .exit_code => unreachable, + }; } + + // Not found + return null; } }; @@ -166,56 +229,35 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { 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.options.parse(&it); + parser.command.semantic_prompt.options_unvalidated = data[2..]; }, 'B' => end_prompt: { parser.command = .{ .semantic_prompt = .init(.end_prompt_start_input) }; if (data.len == 1) break :end_prompt; if (data[1] != ';') break :valid; - var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.options.parse(&it); + parser.command.semantic_prompt.options_unvalidated = data[2..]; }, 'I' => end_prompt_line: { parser.command = .{ .semantic_prompt = .init(.end_prompt_start_input_terminate_eol) }; if (data.len == 1) break :end_prompt_line; if (data[1] != ';') break :valid; - var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.options.parse(&it); + parser.command.semantic_prompt.options_unvalidated = data[2..]; }, 'C' => end_input: { parser.command = .{ .semantic_prompt = .init(.end_input_start_output) }; if (data.len == 1) break :end_input; if (data[1] != ';') break :valid; - var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.options.parse(&it); + parser.command.semantic_prompt.options_unvalidated = data[2..]; }, 'D' => end_command: { parser.command = .{ .semantic_prompt = .init(.end_command) }; if (data.len == 1) break :end_command; if (data[1] != ';') break :valid; - 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.options.exit_code = std.fmt.parseInt( - i32, - key, - 10, - ) catch null; - } - - // Parse the remaining options - parser.command.semantic_prompt.options.parse(&it); + parser.command.semantic_prompt.options_unvalidated = data[2..]; }, 'L' => { @@ -227,16 +269,14 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { 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.options.parse(&it); + parser.command.semantic_prompt.options_unvalidated = data[2..]; }, 'P' => prompt_start: { parser.command = .{ .semantic_prompt = .init(.prompt_start) }; if (data.len == 1) break :prompt_start; if (data[1] != ';') break :valid; - var it = KVIterator.init(writer) catch break :valid; - parser.command.semantic_prompt.options.parse(&it); + parser.command.semantic_prompt.options_unvalidated = data[2..]; }, else => break :valid, @@ -250,81 +290,6 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { 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, - }; - }; - - 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: end_input_start_output" { const testing = std.testing; @@ -336,8 +301,8 @@ test "OSC 133: end_input_start_output" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); try testing.expect(cmd.semantic_prompt.action == .end_input_start_output); - try testing.expect(cmd.semantic_prompt.options.aid == null); - try testing.expect(cmd.semantic_prompt.options.cl == null); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); } test "OSC 133: end_input_start_output extra contents" { @@ -359,7 +324,7 @@ 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.action == .end_input_start_output); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); } test "OSC 133: fresh_line" { @@ -406,8 +371,8 @@ test "OSC 133: fresh_line_new_prompt" { 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.aid == null); - try testing.expect(cmd.semantic_prompt.options.cl == null); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); } test "OSC 133: fresh_line_new_prompt with aid" { @@ -421,7 +386,7 @@ 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.action == .fresh_line_new_prompt); - try testing.expectEqualStrings("14", cmd.semantic_prompt.options.aid.?); + try testing.expectEqualStrings("14", cmd.semantic_prompt.readOption(.aid).?); } test "OSC 133: fresh_line_new_prompt with '=' in aid" { @@ -435,7 +400,7 @@ 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.action == .fresh_line_new_prompt); - try testing.expectEqualStrings("a=b", cmd.semantic_prompt.options.aid.?); + try testing.expectEqualStrings("a=b", cmd.semantic_prompt.readOption(.aid).?); } test "OSC 133: fresh_line_new_prompt with cl=line" { @@ -449,7 +414,7 @@ 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.action == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.options.cl == .line); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line); } test "OSC 133: fresh_line_new_prompt with cl=multiple" { @@ -463,7 +428,7 @@ 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.action == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.options.cl == .multiple); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .multiple); } test "OSC 133: fresh_line_new_prompt with invalid cl" { @@ -477,7 +442,7 @@ 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.action == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.options.cl == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); } test "OSC 133: fresh_line_new_prompt with trailing ;" { @@ -504,8 +469,8 @@ 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.action == .fresh_line_new_prompt); - try testing.expect(cmd.semantic_prompt.options.aid == null); - try testing.expect(cmd.semantic_prompt.options.cl == null); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); } test "OSC 133: fresh_line_new_prompt with multiple options" { @@ -519,8 +484,8 @@ 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.action == .fresh_line_new_prompt); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); - try testing.expect(cmd.semantic_prompt.options.cl == .line); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line); } test "OSC 133: fresh_line_new_prompt default redraw" { @@ -534,7 +499,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 == false); + try testing.expect(cmd.semantic_prompt.readOption(.redraw) == null); } test "OSC 133: fresh_line_new_prompt with redraw=0" { @@ -548,7 +513,7 @@ test "OSC 133: fresh_line_new_prompt with redraw=0" { 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); + try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == false); } test "OSC 133: fresh_line_new_prompt with redraw=1" { @@ -562,7 +527,7 @@ test "OSC 133: fresh_line_new_prompt with redraw=1" { 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.readOption(.redraw).? == true); } test "OSC 133: fresh_line_new_prompt with invalid redraw" { @@ -576,7 +541,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 == false); + try testing.expect(cmd.semantic_prompt.readOption(.redraw) == null); } test "OSC 133: prompt_start" { @@ -590,7 +555,7 @@ test "OSC 133: prompt_start" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); try testing.expect(cmd.semantic_prompt.action == .prompt_start); - try testing.expect(cmd.semantic_prompt.options.prompt_kind == null); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == null); } test "OSC 133: prompt_start with k=i" { @@ -604,7 +569,7 @@ 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.action == .prompt_start); - try testing.expect(cmd.semantic_prompt.options.prompt_kind == .initial); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .initial); } test "OSC 133: prompt_start with k=r" { @@ -618,7 +583,7 @@ 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.action == .prompt_start); - try testing.expect(cmd.semantic_prompt.options.prompt_kind == .right); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .right); } test "OSC 133: prompt_start with k=c" { @@ -632,7 +597,7 @@ 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.action == .prompt_start); - try testing.expect(cmd.semantic_prompt.options.prompt_kind == .continuation); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .continuation); } test "OSC 133: prompt_start with k=s" { @@ -646,7 +611,7 @@ 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.action == .prompt_start); - try testing.expect(cmd.semantic_prompt.options.prompt_kind == .secondary); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .secondary); } test "OSC 133: prompt_start with invalid k" { @@ -660,7 +625,7 @@ 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.action == .prompt_start); - try testing.expect(cmd.semantic_prompt.options.prompt_kind == null); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == null); } test "OSC 133: prompt_start extra contents" { @@ -683,8 +648,8 @@ test "OSC 133: new_command" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); try testing.expect(cmd.semantic_prompt.action == .new_command); - try testing.expect(cmd.semantic_prompt.options.aid == null); - try testing.expect(cmd.semantic_prompt.options.cl == null); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); } test "OSC 133: new_command with aid" { @@ -698,7 +663,7 @@ test "OSC 133: new_command with aid" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); try testing.expect(cmd.semantic_prompt.action == .new_command); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); } test "OSC 133: new_command with cl=line" { @@ -712,7 +677,7 @@ 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.action == .new_command); - try testing.expect(cmd.semantic_prompt.options.cl == .line); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line); } test "OSC 133: new_command with multiple options" { @@ -726,8 +691,8 @@ 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.action == .new_command); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); - try testing.expect(cmd.semantic_prompt.options.cl == .line); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line); } test "OSC 133: new_command extra contents" { @@ -771,7 +736,7 @@ 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.action == .end_prompt_start_input); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); } test "OSC 133: end_prompt_start_input_terminate_eol" { @@ -806,7 +771,7 @@ 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.action == .end_prompt_start_input_terminate_eol); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); } test "OSC 133: end_command" { @@ -820,9 +785,9 @@ test "OSC 133: end_command" { const cmd = p.end(null).?.*; try testing.expect(cmd == .semantic_prompt); try testing.expect(cmd.semantic_prompt.action == .end_command); - try testing.expect(cmd.semantic_prompt.options.exit_code == null); - try testing.expect(cmd.semantic_prompt.options.aid == null); - try testing.expect(cmd.semantic_prompt.options.err == null); + try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == null); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.err) == null); } test "OSC 133: end_command extra contents" { @@ -845,7 +810,7 @@ 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.action == .end_command); - try testing.expect(cmd.semantic_prompt.options.exit_code == 0); + try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == 0); } test "OSC 133: end_command with exit code and aid" { @@ -859,6 +824,78 @@ 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.action == .end_command); - try testing.expectEqualStrings("foo", cmd.semantic_prompt.options.aid.?); - try testing.expect(cmd.semantic_prompt.options.exit_code == 12); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); + try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == 12); +} + +test "Option.read aid" { + const testing = std.testing; + try testing.expectEqualStrings("test123", Option.aid.read("aid=test123").?); + try testing.expectEqualStrings("myaid", Option.aid.read("cl=line;aid=myaid;k=i").?); + try testing.expect(Option.aid.read("cl=line;k=i") == null); + try testing.expectEqualStrings("", Option.aid.read("aid=").?); + try testing.expectEqualStrings("last", Option.aid.read("k=i;aid=last").?); + try testing.expectEqualStrings("first", Option.aid.read("aid=first;k=i").?); + try testing.expect(Option.aid.read("") == null); + try testing.expect(Option.aid.read("aid") == null); + try testing.expectEqualStrings("value", Option.aid.read(";;aid=value;;").?); +} + +test "Option.read cl" { + const testing = std.testing; + try testing.expect(Option.cl.read("cl=line").? == .line); + try testing.expect(Option.cl.read("cl=multiple").? == .multiple); + try testing.expect(Option.cl.read("cl=conservative_vertical").? == .conservative_vertical); + try testing.expect(Option.cl.read("cl=smart_vertical").? == .smart_vertical); + try testing.expect(Option.cl.read("cl=invalid") == null); + try testing.expect(Option.cl.read("aid=foo") == null); +} + +test "Option.read prompt_kind" { + const testing = std.testing; + try testing.expect(Option.prompt_kind.read("k=i").? == .initial); + try testing.expect(Option.prompt_kind.read("k=r").? == .right); + try testing.expect(Option.prompt_kind.read("k=c").? == .continuation); + try testing.expect(Option.prompt_kind.read("k=s").? == .secondary); + try testing.expect(Option.prompt_kind.read("k=x") == null); + try testing.expect(Option.prompt_kind.read("k=ii") == null); + try testing.expect(Option.prompt_kind.read("k=") == null); +} + +test "Option.read err" { + const testing = std.testing; + try testing.expectEqualStrings("some_error", Option.err.read("err=some_error").?); + try testing.expect(Option.err.read("aid=foo") == null); +} + +test "Option.read redraw" { + const testing = std.testing; + try testing.expect(Option.redraw.read("redraw=1").? == true); + try testing.expect(Option.redraw.read("redraw=0").? == false); + try testing.expect(Option.redraw.read("redraw=2") == null); + try testing.expect(Option.redraw.read("redraw=10") == null); + try testing.expect(Option.redraw.read("redraw=") == null); +} + +test "Option.read special_key" { + const testing = std.testing; + try testing.expect(Option.special_key.read("special_key=1").? == true); + try testing.expect(Option.special_key.read("special_key=0").? == false); + try testing.expect(Option.special_key.read("special_key=x") == null); +} + +test "Option.read click_events" { + const testing = std.testing; + try testing.expect(Option.click_events.read("click_events=1").? == true); + try testing.expect(Option.click_events.read("click_events=0").? == false); + try testing.expect(Option.click_events.read("click_events=yes") == null); +} + +test "Option.read exit_code" { + const testing = std.testing; + try testing.expect(Option.exit_code.read("42").? == 42); + try testing.expect(Option.exit_code.read("0").? == 0); + try testing.expect(Option.exit_code.read("-1").? == -1); + try testing.expect(Option.exit_code.read("abc") == null); + try testing.expect(Option.exit_code.read("127;aid=foo").? == 127); } diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index 57227a057..18ed0dd42 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -215,11 +215,13 @@ pub const Handler = struct { ) void { switch (cmd.action) { .fresh_line_new_prompt => { - const kind = cmd.options.prompt_kind orelse .initial; + const kind = cmd.readOption(.prompt_kind) orelse .initial; switch (kind) { .initial, .right => { self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt; - self.terminal.flags.shell_redraws_prompt = cmd.options.redraw; + if (cmd.readOption(.redraw)) |redraw| { + self.terminal.flags.shell_redraws_prompt = redraw; + } }, .continuation, .secondary => { self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index cfe68fd1c..63094b106 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1072,11 +1072,13 @@ pub const StreamHandler = struct { ) void { switch (cmd.action) { .fresh_line_new_prompt => { - const kind = cmd.options.prompt_kind orelse .initial; + const kind = cmd.readOption(.prompt_kind) orelse .initial; switch (kind) { .initial, .right => { self.terminal.markSemanticPrompt(.prompt); - self.terminal.flags.shell_redraws_prompt = cmd.options.redraw; + if (cmd.readOption(.redraw)) |redraw| { + self.terminal.flags.shell_redraws_prompt = redraw; + } }, .continuation, .secondary => { self.terminal.markSemanticPrompt(.prompt_continuation); @@ -1094,7 +1096,7 @@ pub const StreamHandler = struct { // 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; + const raw: i32 = cmd.readOption(.exit_code) orelse 0; break :code std.math.cast(u8, raw) orelse 1; };