diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index 1eaa659d2..00ea3618f 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -1,4 +1,5 @@ #include +#include #include 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; } diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 12ed2d015..5d80cb653 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -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 diff --git a/src/lib/enum.zig b/src/lib/enum.zig new file mode 100644 index 000000000..c3971ebde --- /dev/null +++ b/src/lib/enum.zig @@ -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)); + } +} diff --git a/src/lib/main.zig b/src/lib/main.zig new file mode 100644 index 000000000..4ef8dcb2d --- /dev/null +++ b/src/lib/main.zig @@ -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()); +} diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 656509cce..37ab7ae68 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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"); } diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 1f2e814f6..6deb03da5 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -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 = {} }, diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig index 1b0449bbf..2085e2243 100644 --- a/src/terminal/build_options.zig +++ b/src/terminal/build_options.zig @@ -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, diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig new file mode 100644 index 000000000..f32dd226f --- /dev/null +++ b/src/terminal/c/main.zig @@ -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"); +} diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig new file mode 100644 index 000000000..c04626b69 --- /dev/null +++ b/src/terminal/c/osc.zig @@ -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)); +} diff --git a/src/terminal/c/result.zig b/src/terminal/c/result.zig new file mode 100644 index 000000000..a2ebc9b69 --- /dev/null +++ b/src/terminal/c/result.zig @@ -0,0 +1,5 @@ +/// C: GhosttyResult +pub const Result = enum(c_int) { + success = 0, + out_of_memory = -1, +}; diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig deleted file mode 100644 index 194a91d6d..000000000 --- a/src/terminal/c_api.zig +++ /dev/null @@ -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); -} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 4064c0c9c..7403ff309 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -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()); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index bd7337b42..71d2f8598 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -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); }