mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-13 19:15:48 +00:00
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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user