lib-vt C: More OSC parsing API (#8941)

This adds more of the OSC parsing API to the C library of
`libghostty-vt`. This adds the following:

* `ghostty_osc_next` - Push a single character into the OSC parser
* `ghostty_osc_reset` - Reset the parser state and free any temporary
memory
* `ghostty_osc_end` - End a parsing sequence and return the parsed
command
* `ghostty_osc_command_type` - Return the type of command parsed
* `examples/c-vt` is updated to use these new APIs
* Our Zig `osc.Command` tagged union is updated to use a new comptime
func to generate an ABI compatible tag for the C lib.
* **Important change:** Our Zig `osc.Parser.end` function was modified
to return a _pointer_ to the command within its own structure memory
rather than a copy. This eases the C API. This impacts the Zig side but
we ultimately copy it again [for now] in the main `terminal.Parser` so
end result is the same.
* Unit tests cover even the C lib
* Shuffled some code for maintainability

The plan is to use this opaque type with getters to ease ABI
compatibility going forward.
pull/8945/head
Mitchell Hashimoto 2025-09-28 13:37:23 -07:00 committed by GitHub
commit b73fdd4326
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 449 additions and 132 deletions

View File

@ -1,4 +1,5 @@
#include <stddef.h>
#include <stdio.h>
#include <ghostty/vt.h>
int main() {
@ -6,6 +7,19 @@ int main() {
if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) {
return 1;
}
// Setup change window title command to change the title to "a"
ghostty_osc_next(parser, '0');
ghostty_osc_next(parser, ';');
ghostty_osc_next(parser, 'a');
// End parsing and get command
GhosttyOscCommand command = ghostty_osc_end(parser, 0);
// Get and print command type
GhosttyOscCommandType type = ghostty_osc_command_type(command);
printf("Command type: %d\n", type);
ghostty_osc_free(parser);
return 0;
}

View File

@ -35,6 +35,15 @@ extern "C" {
*/
typedef struct GhosttyOscParser *GhosttyOscParser;
/**
* Opaque handle to a single OSC command.
*
* This handle represents a parsed OSC (Operating System Command) command.
* The command can be queried for its type and associated data using
* `ghostty_osc_command_type` and `ghostty_osc_command_data`.
*/
typedef struct GhosttyOscCommand *GhosttyOscCommand;
/**
* Result codes for libghostty-vt operations.
*/
@ -45,6 +54,33 @@ typedef enum {
GHOSTTY_OUT_OF_MEMORY = -1,
} GhosttyResult;
/**
* OSC command types.
*/
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,
} GhosttyOscCommandType;
//-------------------------------------------------------------------
// Allocator Interface
@ -214,6 +250,72 @@ GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParse
*/
void ghostty_osc_free(GhosttyOscParser parser);
/**
* Reset an OSC parser instance to its initial state.
*
* Resets the parser state, clearing any partially parsed OSC sequences
* and returning the parser to its initial state. This is useful for
* reusing a parser instance or recovering from parse errors.
*
* @param parser The parser handle to reset, must not be null.
*/
void ghostty_osc_reset(GhosttyOscParser parser);
/**
* Parse the next byte in an OSC sequence.
*
* Processes a single byte as part of an OSC sequence. The parser maintains
* internal state to track the progress through the sequence. Call this
* function for each byte in the sequence data.
*
* When finished pumping the parser with bytes, call ghostty_osc_end
* to get the final result.
*
* @param parser The parser handle, must not be null.
* @param byte The next byte to parse
*/
void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte);
/**
* Finalize OSC parsing and retrieve the parsed command.
*
* Call this function after feeding all bytes of an OSC sequence to the parser
* using ghostty_osc_next() with the exception of the terminating character
* (ESC or ST). This function finalizes the parsing process and returns the
* parsed OSC command.
*
* The return value is never NULL. Invalid commands will return a command
* with type GHOSTTY_OSC_COMMAND_INVALID.
*
* The terminator parameter specifies the byte that terminated the OSC sequence
* (typically 0x07 for BEL or 0x5C for ST after ESC). This information is
* preserved in the parsed command so that responses can use the same terminator
* format for better compatibility with the calling program. For commands that
* do not require a response, this parameter is ignored and the resulting
* command will not retain the terminator information.
*
* The returned command handle is valid until the next call to any
* `ghostty_osc_*` function with the same parser instance with the exception
* of command introspection functions such as `ghostty_osc_command_type`.
*
* @param parser The parser handle, must not be null.
* @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST)
* @return Handle to the parsed OSC command
*/
GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator);
/**
* Get the type of an OSC command.
*
* Returns the type identifier for the given OSC command. This can be used
* to determine what kind of command was parsed and what data might be
* available from it.
*
* @param command The OSC command handle to query (may be NULL)
* @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL
*/
GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command);
#ifdef __cplusplus
}
#endif

97
src/lib/enum.zig Normal file
View File

@ -0,0 +1,97 @@
const std = @import("std");
/// Create an enum type with the given keys that is C ABI compatible
/// if we're targeting C, otherwise a Zig enum with smallest possible
/// backing type.
///
/// In all cases, the enum keys will be created in the order given.
/// For C ABI, this means that the order MUST NOT be changed in order
/// to preserve ABI compatibility. You can set a key to null to
/// remove it from the Zig enum while keeping the "hole" in the C enum
/// to preserve ABI compatibility.
///
/// C detection is up to the caller, since there are multiple ways
/// to do that. We rely on the `target` parameter to determine whether we
/// should create a C compatible enum or a Zig enum.
///
/// For the Zig enum, the enum value is not guaranteed to be stable, so
/// it shouldn't be relied for things like serialization.
pub fn Enum(
target: Target,
keys: []const ?[:0]const u8,
) type {
var fields: [keys.len]std.builtin.Type.EnumField = undefined;
var fields_i: usize = 0;
var holes: usize = 0;
for (keys) |key_| {
const key: [:0]const u8 = key_ orelse {
switch (target) {
// For Zig we don't track holes because the enum value
// isn't guaranteed to be stable and we want to use the
// smallest possible backing type.
.zig => {},
// For C we must track holes to preserve ABI compatibility
// with subsequent values.
.c => holes += 1,
}
continue;
};
fields[fields_i] = .{
.name = key,
.value = fields_i + holes,
};
fields_i += 1;
}
// Assigned to var so that the type name is nicer in stack traces.
const Result = @Type(.{ .@"enum" = .{
.tag_type = switch (target) {
.c => c_int,
.zig => std.math.IntFittingRange(0, fields_i - 1),
},
.fields = fields[0..fields_i],
.decls = &.{},
.is_exhaustive = true,
} });
return Result;
}
pub const Target = union(enum) {
c,
zig,
};
test "zig" {
const testing = std.testing;
const T = Enum(.zig, &.{ "a", "b", "c", "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(u2, info.tag_type);
}
test "c" {
const testing = std.testing;
const T = Enum(.c, &.{ "a", "b", "c", "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(c_int, info.tag_type);
}
test "abi by removing a key" {
const testing = std.testing;
// C
{
const T = Enum(.c, &.{ "a", "b", null, "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(c_int, info.tag_type);
try testing.expectEqual(3, @intFromEnum(T.d));
}
// Zig
{
const T = Enum(.zig, &.{ "a", "b", null, "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(u2, info.tag_type);
try testing.expectEqual(2, @intFromEnum(T.d));
}
}

10
src/lib/main.zig Normal file
View File

@ -0,0 +1,10 @@
const std = @import("std");
const enumpkg = @import("enum.zig");
pub const allocator = @import("allocator.zig");
pub const Enum = enumpkg.Enum;
pub const EnumTarget = enumpkg.Target;
test {
std.testing.refAllDecls(@This());
}

View File

@ -72,12 +72,17 @@ comptime {
const c = terminal.c_api;
@export(&c.osc_new, .{ .name = "ghostty_osc_new" });
@export(&c.osc_free, .{ .name = "ghostty_osc_free" });
@export(&c.osc_next, .{ .name = "ghostty_osc_next" });
@export(&c.osc_reset, .{ .name = "ghostty_osc_reset" });
@export(&c.osc_end, .{ .name = "ghostty_osc_end" });
@export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" });
}
}
test {
_ = terminal;
// Tests always test the C API
// Tests always test the C API and shared C functions
_ = terminal.c_api;
_ = @import("lib/main.zig");
}

View File

@ -274,7 +274,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action {
// Exit depends on current state
if (self.state == next_state) null else switch (self.state) {
.osc_string => if (self.osc_parser.end(c)) |cmd|
Action{ .osc_dispatch = cmd }
Action{ .osc_dispatch = cmd.* }
else
null,
.dcs_passthrough => Action{ .dcs_unhook = {} },

View File

@ -1,5 +1,8 @@
const std = @import("std");
/// True if we're building the C library libghostty-vt.
pub const is_c_lib = @import("root") == @import("../lib_vt.zig");
pub const Options = struct {
/// The target artifact to build. This will gate some functionality.
artifact: Artifact,

16
src/terminal/c/main.zig Normal file
View File

@ -0,0 +1,16 @@
pub const osc = @import("osc.zig");
// The full C API, unexported.
pub const osc_new = osc.new;
pub const osc_free = osc.free;
pub const osc_reset = osc.reset;
pub const osc_next = osc.next;
pub const osc_end = osc.end;
pub const osc_command_type = osc.commandType;
test {
_ = osc;
// We want to make sure we run the tests for the C allocator interface.
_ = @import("../../lib/allocator.zig");
}

81
src/terminal/c/osc.zig Normal file
View File

@ -0,0 +1,81 @@
const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin");
const lib_alloc = @import("../../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const osc = @import("../osc.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyOscParser
pub const Parser = ?*osc.Parser;
/// C: GhosttyOscCommand
pub const Command = ?*osc.Command;
pub fn new(
alloc_: ?*const CAllocator,
result: *Parser,
) callconv(.c) Result {
const alloc = lib_alloc.default(alloc_);
const ptr = alloc.create(osc.Parser) catch
return .out_of_memory;
ptr.* = .initAlloc(alloc);
result.* = ptr;
return .success;
}
pub fn free(parser_: Parser) callconv(.c) void {
// C-built parsers always have an associated allocator.
const parser = parser_ orelse return;
const alloc = parser.alloc.?;
parser.deinit();
alloc.destroy(parser);
}
pub fn reset(parser_: Parser) callconv(.c) void {
parser_.?.reset();
}
pub fn next(parser_: Parser, byte: u8) callconv(.c) void {
parser_.?.next(byte);
}
pub fn end(parser_: Parser, terminator: u8) callconv(.c) Command {
return parser_.?.end(terminator);
}
pub fn commandType(command_: Command) callconv(.c) osc.Command.Key {
const command = command_ orelse return .invalid;
return command.*;
}
test "alloc" {
const testing = std.testing;
var p: Parser = undefined;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&p,
));
free(p);
}
test "command type null" {
const testing = std.testing;
try testing.expectEqual(.invalid, commandType(null));
}
test "command type" {
const testing = std.testing;
var p: Parser = undefined;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&p,
));
defer free(p);
p.next('0');
p.next(';');
p.next('a');
const cmd = p.end(0);
try testing.expectEqual(.change_window_title, commandType(cmd));
}

View File

@ -0,0 +1,5 @@
/// C: GhosttyResult
pub const Result = enum(c_int) {
success = 0,
out_of_memory = -1,
};

View File

@ -1,49 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin");
const lib_alloc = @import("../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const osc = @import("osc.zig");
/// C: GhosttyOscParser
pub const OscParser = ?*osc.Parser;
/// C: GhosttyResult
pub const Result = enum(c_int) {
success = 0,
out_of_memory = -1,
};
pub fn osc_new(
alloc_: ?*const CAllocator,
result: *OscParser,
) callconv(.c) Result {
const alloc = lib_alloc.default(alloc_);
const ptr = alloc.create(osc.Parser) catch
return .out_of_memory;
ptr.* = .initAlloc(alloc);
result.* = ptr;
return .success;
}
pub fn osc_free(parser_: OscParser) callconv(.c) void {
// C-built parsers always have an associated allocator.
const parser = parser_ orelse return;
const alloc = parser.alloc.?;
parser.deinit();
alloc.destroy(parser);
}
test {
_ = lib_alloc;
}
test "osc" {
const testing = std.testing;
var p: OscParser = undefined;
try testing.expectEqual(Result.success, osc_new(
&lib_alloc.test_allocator,
&p,
));
osc_free(p);
}

View File

@ -63,8 +63,8 @@ pub const Attribute = sgr.Attribute;
pub const isSafePaste = sanitize.isSafePaste;
/// This is set to true when we're building the C library.
pub const is_c_lib = @import("root") == @import("../lib_vt.zig");
pub const c_api = @import("c_api.zig");
pub const is_c_lib = @import("build_options.zig").is_c_lib;
pub const c_api = if (is_c_lib) @import("c/main.zig") else void;
test {
@import("std").testing.refAllDecls(@This());

View File

@ -10,6 +10,8 @@ const builtin = @import("builtin");
const mem = std.mem;
const assert = std.debug.assert;
const Allocator = mem.Allocator;
const LibEnum = @import("../lib/enum.zig").Enum;
const is_c_lib = @import("build_options.zig").is_c_lib;
const RGB = @import("color.zig").RGB;
const kitty_color = @import("kitty/color.zig");
const osc_color = @import("osc/color.zig");
@ -17,7 +19,7 @@ pub const color = osc_color;
const log = std.log.scoped(.osc);
pub const Command = union(enum) {
pub const Command = union(Key) {
/// This generally shouldn't ever be set except as an initial zero value.
/// Ignore it.
invalid,
@ -172,6 +174,34 @@ pub const Command = union(enum) {
/// ConEmu GUI macro (OSC 9;6)
conemu_guimacro: []const u8,
pub const Key = LibEnum(
if (is_c_lib) .c else .zig,
// NOTE: Order matters, see LibEnum documentation.
&.{
"invalid",
"change_window_title",
"change_window_icon",
"prompt_start",
"prompt_end",
"end_of_input",
"end_of_command",
"clipboard_contents",
"report_pwd",
"mouse_shape",
"color_operation",
"kitty_color_protocol",
"show_desktop_notification",
"hyperlink_start",
"hyperlink_end",
"conemu_sleep",
"conemu_show_message_box",
"conemu_change_tab_title",
"conemu_progress_report",
"conemu_wait_input",
"conemu_guimacro",
},
);
pub const ProgressReport = struct {
pub const State = enum(c_int) {
remove,
@ -431,7 +461,7 @@ pub const Parser = struct {
self.reset();
}
/// Reset the parser start.
/// Reset the parser state.
pub fn reset(self: *Parser) void {
// If the state is already empty then we do nothing because
// we may touch uninitialized memory.
@ -1567,7 +1597,10 @@ pub const Parser = struct {
/// is null, then no valid command was found. The optional terminator_ch
/// is the final character in the OSC sequence. This is used to determine
/// the response terminator.
pub fn end(self: *Parser, terminator_ch: ?u8) ?Command {
///
/// The returned pointer is only valid until the next call to the parser.
/// Callers should copy out any data they wish to retain across calls.
pub fn end(self: *Parser, terminator_ch: ?u8) ?*Command {
if (!self.complete) {
if (comptime !builtin.is_test) log.warn(
"invalid OSC command: {s}",
@ -1626,7 +1659,7 @@ pub const Parser = struct {
else => {},
}
return self.command;
return &self.command;
}
};
@ -1642,7 +1675,7 @@ test "OSC: change_window_title" {
p.next(';');
p.next('a');
p.next('b');
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .change_window_title);
try testing.expectEqualStrings("ab", cmd.change_window_title);
}
@ -1655,7 +1688,7 @@ test "OSC: change_window_title with 2" {
p.next(';');
p.next('a');
p.next('b');
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .change_window_title);
try testing.expectEqualStrings("ab", cmd.change_window_title);
}
@ -1677,7 +1710,7 @@ test "OSC: change_window_title with utf8" {
p.next(0xE2);
p.next(0x80);
p.next(0x90);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .change_window_title);
try testing.expectEqualStrings("", cmd.change_window_title);
}
@ -1688,7 +1721,7 @@ test "OSC: change_window_title empty" {
var p: Parser = .init();
p.next('2');
p.next(';');
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .change_window_title);
try testing.expectEqualStrings("", cmd.change_window_title);
}
@ -1701,7 +1734,7 @@ test "OSC: change_window_icon" {
p.next(';');
p.next('a');
p.next('b');
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .change_window_icon);
try testing.expectEqualStrings("ab", cmd.change_window_icon);
}
@ -1714,7 +1747,7 @@ test "OSC: prompt_start" {
const input = "133;A";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
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);
@ -1728,7 +1761,7 @@ test "OSC: prompt_start with single option" {
const input = "133;A;aid=14";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .prompt_start);
try testing.expectEqualStrings("14", cmd.prompt_start.aid.?);
}
@ -1741,7 +1774,7 @@ test "OSC: prompt_start with redraw disabled" {
const input = "133;A;redraw=0";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .prompt_start);
try testing.expect(!cmd.prompt_start.redraw);
}
@ -1754,7 +1787,7 @@ test "OSC: prompt_start with redraw invalid value" {
const input = "133;A;redraw=42";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
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);
@ -1768,7 +1801,7 @@ test "OSC: prompt_start with continuation" {
const input = "133;A;k=c";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .prompt_start);
try testing.expect(cmd.prompt_start.kind == .continuation);
}
@ -1781,7 +1814,7 @@ test "OSC: prompt_start with secondary" {
const input = "133;A;k=s";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .prompt_start);
try testing.expect(cmd.prompt_start.kind == .secondary);
}
@ -1794,7 +1827,7 @@ test "OSC: end_of_command no exit code" {
const input = "133;D";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .end_of_command);
}
@ -1806,7 +1839,7 @@ test "OSC: end_of_command with exit code" {
const input = "133;D;25";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .end_of_command);
try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?);
}
@ -1819,7 +1852,7 @@ test "OSC: prompt_end" {
const input = "133;B";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .prompt_end);
}
@ -1831,7 +1864,7 @@ test "OSC: end_of_input" {
const input = "133;C";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .end_of_input);
}
@ -1843,7 +1876,7 @@ test "OSC: get/set clipboard" {
const input = "52;s;?";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .clipboard_contents);
try testing.expect(cmd.clipboard_contents.kind == 's');
try testing.expectEqualStrings("?", cmd.clipboard_contents.data);
@ -1857,7 +1890,7 @@ test "OSC: get/set clipboard (optional parameter)" {
const input = "52;;?";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .clipboard_contents);
try testing.expect(cmd.clipboard_contents.kind == 'c');
try testing.expectEqualStrings("?", cmd.clipboard_contents.data);
@ -1872,7 +1905,7 @@ test "OSC: get/set clipboard with allocator" {
const input = "52;s;?";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .clipboard_contents);
try testing.expect(cmd.clipboard_contents.kind == 's');
try testing.expectEqualStrings("?", cmd.clipboard_contents.data);
@ -1887,7 +1920,7 @@ test "OSC: clear clipboard" {
const input = "52;;";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .clipboard_contents);
try testing.expect(cmd.clipboard_contents.kind == 'c');
try testing.expectEqualStrings("", cmd.clipboard_contents.data);
@ -1901,7 +1934,7 @@ test "OSC: report pwd" {
const input = "7;file:///tmp/example";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .report_pwd);
try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value);
}
@ -1913,7 +1946,7 @@ test "OSC: report pwd empty" {
const input = "7;";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .report_pwd);
try testing.expectEqualStrings("", cmd.report_pwd.value);
}
@ -1926,7 +1959,7 @@ test "OSC: pointer cursor" {
const input = "22;pointer";
for (input) |ch| p.next(ch);
const cmd = p.end(null).?;
const cmd = p.end(null).?.*;
try testing.expect(cmd == .mouse_shape);
try testing.expectEqualStrings("pointer", cmd.mouse_shape.value);
}
@ -1951,7 +1984,7 @@ test "OSC: OSC 9;1 ConEmu sleep" {
const input = "9;1;420";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_sleep);
try testing.expectEqual(420, cmd.conemu_sleep.duration_ms);
@ -1965,7 +1998,7 @@ test "OSC: OSC 9;1 ConEmu sleep with no value default to 100ms" {
const input = "9;1;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_sleep);
try testing.expectEqual(100, cmd.conemu_sleep.duration_ms);
@ -1979,7 +2012,7 @@ test "OSC: OSC 9;1 conemu sleep cannot exceed 10000ms" {
const input = "9;1;12345";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_sleep);
try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms);
@ -1993,7 +2026,7 @@ test "OSC: OSC 9;1 conemu sleep invalid input" {
const input = "9;1;foo";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_sleep);
try testing.expectEqual(100, cmd.conemu_sleep.duration_ms);
@ -2007,7 +2040,7 @@ test "OSC: OSC 9;1 conemu sleep -> desktop notification 1" {
const input = "9;1";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("1", cmd.show_desktop_notification.body);
@ -2021,7 +2054,7 @@ test "OSC: OSC 9;1 conemu sleep -> desktop notification 2" {
const input = "9;1a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body);
@ -2035,7 +2068,7 @@ test "OSC: OSC 9 show desktop notification" {
const input = "9;Hello world";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("", cmd.show_desktop_notification.title);
try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body);
@ -2049,7 +2082,7 @@ test "OSC: OSC 9 show single character desktop notification" {
const input = "9;H";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("", cmd.show_desktop_notification.title);
try testing.expectEqualStrings("H", cmd.show_desktop_notification.body);
@ -2063,7 +2096,7 @@ test "OSC: OSC 777 show desktop notification with title" {
const input = "777;notify;Title;Body";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title");
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body");
@ -2077,7 +2110,7 @@ test "OSC: OSC 9;2 ConEmu message box" {
const input = "9;2;hello world";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_show_message_box);
try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box);
}
@ -2090,7 +2123,7 @@ test "OSC: 9;2 ConEmu message box invalid input" {
const input = "9;2";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("2", cmd.show_desktop_notification.body);
}
@ -2103,7 +2136,7 @@ test "OSC: 9;2 ConEmu message box empty message" {
const input = "9;2;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_show_message_box);
try testing.expectEqualStrings("", cmd.conemu_show_message_box);
}
@ -2116,7 +2149,7 @@ test "OSC: 9;2 ConEmu message box spaces only message" {
const input = "9;2; ";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_show_message_box);
try testing.expectEqualStrings(" ", cmd.conemu_show_message_box);
}
@ -2129,7 +2162,7 @@ test "OSC: OSC 9;2 message box -> desktop notification 1" {
const input = "9;2";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("2", cmd.show_desktop_notification.body);
@ -2143,7 +2176,7 @@ test "OSC: OSC 9;2 message box -> desktop notification 2" {
const input = "9;2a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body);
@ -2157,7 +2190,7 @@ test "OSC: 9;3 ConEmu change tab title" {
const input = "9;3;foo bar";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_change_tab_title);
try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value);
}
@ -2170,7 +2203,7 @@ test "OSC: 9;3 ConEmu change tab title reset" {
const input = "9;3;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
const expected_command: Command = .{ .conemu_change_tab_title = .reset };
try testing.expectEqual(expected_command, cmd);
@ -2184,7 +2217,7 @@ test "OSC: 9;3 ConEmu change tab title spaces only" {
const input = "9;3; ";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_change_tab_title);
try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value);
@ -2198,7 +2231,7 @@ test "OSC: OSC 9;3 change tab title -> desktop notification 1" {
const input = "9;3";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("3", cmd.show_desktop_notification.body);
@ -2212,7 +2245,7 @@ test "OSC: OSC 9;3 message box -> desktop notification 2" {
const input = "9;3a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body);
@ -2226,7 +2259,7 @@ test "OSC: OSC 9;4 ConEmu progress set" {
const input = "9;4;1;100";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expect(cmd.conemu_progress_report.progress == 100);
@ -2240,7 +2273,7 @@ test "OSC: OSC 9;4 ConEmu progress set overflow" {
const input = "9;4;1;900";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expectEqual(100, cmd.conemu_progress_report.progress);
@ -2254,7 +2287,7 @@ test "OSC: OSC 9;4 ConEmu progress set single digit" {
const input = "9;4;1;9";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expect(cmd.conemu_progress_report.progress == 9);
@ -2268,7 +2301,7 @@ test "OSC: OSC 9;4 ConEmu progress set double digit" {
const input = "9;4;1;94";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expectEqual(94, cmd.conemu_progress_report.progress);
@ -2282,7 +2315,7 @@ test "OSC: OSC 9;4 ConEmu progress set extra semicolon ignored" {
const input = "9;4;1;100";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expectEqual(100, cmd.conemu_progress_report.progress);
@ -2296,7 +2329,7 @@ test "OSC: OSC 9;4 ConEmu progress remove with no progress" {
const input = "9;4;0;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .remove);
try testing.expect(cmd.conemu_progress_report.progress == null);
@ -2310,7 +2343,7 @@ test "OSC: OSC 9;4 ConEmu progress remove with double semicolon" {
const input = "9;4;0;;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .remove);
try testing.expect(cmd.conemu_progress_report.progress == null);
@ -2324,7 +2357,7 @@ test "OSC: OSC 9;4 ConEmu progress remove ignores progress" {
const input = "9;4;0;100";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .remove);
try testing.expect(cmd.conemu_progress_report.progress == null);
@ -2338,7 +2371,7 @@ test "OSC: OSC 9;4 ConEmu progress remove extra semicolon" {
const input = "9;4;0;100;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .remove);
}
@ -2351,7 +2384,7 @@ test "OSC: OSC 9;4 ConEmu progress error" {
const input = "9;4;2";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .@"error");
try testing.expect(cmd.conemu_progress_report.progress == null);
@ -2365,7 +2398,7 @@ test "OSC: OSC 9;4 ConEmu progress error with progress" {
const input = "9;4;2;100";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .@"error");
try testing.expect(cmd.conemu_progress_report.progress == 100);
@ -2379,7 +2412,7 @@ test "OSC: OSC 9;4 progress pause" {
const input = "9;4;4";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .pause);
try testing.expect(cmd.conemu_progress_report.progress == null);
@ -2393,7 +2426,7 @@ test "OSC: OSC 9;4 ConEmu progress pause with progress" {
const input = "9;4;4;100";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .pause);
try testing.expect(cmd.conemu_progress_report.progress == 100);
@ -2407,7 +2440,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 1" {
const input = "9;4";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("4", cmd.show_desktop_notification.body);
@ -2421,7 +2454,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 2" {
const input = "9;4;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body);
@ -2435,7 +2468,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 3" {
const input = "9;4;5";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body);
@ -2449,7 +2482,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 4" {
const input = "9;4;5a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body);
@ -2463,7 +2496,7 @@ test "OSC: OSC 9;5 ConEmu wait input" {
const input = "9;5";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_wait_input);
}
@ -2475,7 +2508,7 @@ test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" {
const input = "9;5;foo";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_wait_input);
}
@ -2499,7 +2532,7 @@ test "OSC: hyperlink" {
const input = "8;;http://example.com";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .hyperlink_start);
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
}
@ -2512,7 +2545,7 @@ test "OSC: hyperlink with id set" {
const input = "8;id=foo;http://example.com";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .hyperlink_start);
try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo");
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
@ -2526,7 +2559,7 @@ test "OSC: hyperlink with empty id" {
const input = "8;id=;http://example.com";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .hyperlink_start);
try testing.expectEqual(null, cmd.hyperlink_start.id);
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
@ -2540,7 +2573,7 @@ test "OSC: hyperlink with incomplete key" {
const input = "8;id;http://example.com";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .hyperlink_start);
try testing.expectEqual(null, cmd.hyperlink_start.id);
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
@ -2554,7 +2587,7 @@ test "OSC: hyperlink with empty key" {
const input = "8;=value;http://example.com";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .hyperlink_start);
try testing.expectEqual(null, cmd.hyperlink_start.id);
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
@ -2568,7 +2601,7 @@ test "OSC: hyperlink with empty key and id" {
const input = "8;=value:id=foo;http://example.com";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .hyperlink_start);
try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo");
try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com");
@ -2594,7 +2627,7 @@ test "OSC: hyperlink end" {
const input = "8;;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .hyperlink_end);
}
@ -2608,7 +2641,7 @@ test "OSC: kitty color protocol" {
const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .kitty_color_protocol);
try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len);
{
@ -2690,7 +2723,7 @@ test "OSC: kitty color protocol double reset" {
const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .kitty_color_protocol);
p.reset();
@ -2706,7 +2739,7 @@ test "OSC: kitty color protocol reset after invalid" {
const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .kitty_color_protocol);
p.reset();
@ -2727,7 +2760,7 @@ test "OSC: kitty color protocol no key" {
const input = "21;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .kitty_color_protocol);
try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len);
}
@ -2741,7 +2774,7 @@ test "OSC: 9;6: ConEmu guimacro 1" {
const input = "9;6;a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_guimacro);
try testing.expectEqualStrings("a", cmd.conemu_guimacro);
}
@ -2755,7 +2788,7 @@ test "OSC: 9;6: ConEmu guimacro 2" {
const input = "9;6;ab";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .conemu_guimacro);
try testing.expectEqualStrings("ab", cmd.conemu_guimacro);
}
@ -2769,7 +2802,7 @@ test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" {
const input = "9;6";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
const cmd = p.end('\x1b').?.*;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("6", cmd.show_desktop_notification.body);
}