diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 43824ce01..93d413854 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -155,6 +155,10 @@ pub const Command = union(Key) { kitty_clipboard_protocol: KittyClipboardProtocol, + /// OSC 3008. Hierarchical context signalling (UAPI spec). + /// https://uapi-group.org/specifications/specs/osc_context/ + context_signal: parsers.context_signal.Command, + pub const SemanticPrompt = parsers.semantic_prompt.Command; pub const KittyClipboardProtocol = parsers.kitty_clipboard_protocol.OSC; @@ -187,6 +191,7 @@ pub const Command = union(Key) { "conemu_comment", "kitty_text_sizing", "kitty_clipboard_protocol", + "context_signal", }, ); @@ -226,7 +231,6 @@ pub const Command = union(Key) { 8 => 64, else => unreachable, }); - // @compileLog(@sizeOf(Command)); } }; @@ -311,12 +315,16 @@ pub const Parser = struct { @"0", @"1", @"2", + @"3", @"4", @"5", @"6", @"7", @"8", @"9", + @"30", + @"300", + @"3008", @"10", @"11", @"12", @@ -411,6 +419,7 @@ pub const Parser = struct { .show_desktop_notification, .kitty_text_sizing, .kitty_clipboard_protocol, + .context_signal, => {}, } @@ -488,6 +497,7 @@ pub const Parser = struct { '0' => self.state = .@"0", '1' => self.state = .@"1", '2' => self.state = .@"2", + '3' => self.state = .@"3", '4' => self.state = .@"4", '5' => self.state = .@"5", '6' => self.state = .@"6", @@ -497,6 +507,26 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"3" => switch (c) { + '0' => self.state = .@"30", + else => self.state = .invalid, + }, + + .@"30" => switch (c) { + '0' => self.state = .@"300", + else => self.state = .invalid, + }, + + .@"300" => switch (c) { + '8' => self.state = .@"3008", + else => self.state = .invalid, + }, + + .@"3008" => switch (c) { + ';' => self.writeToFixed(), + else => self.state = .invalid, + }, + .@"1" => switch (c) { ';' => self.writeToFixed(), '0' => self.state = .@"10", @@ -704,6 +734,13 @@ pub const Parser = struct { .@"55" => null, + .@"3", + .@"30", + .@"300", + => null, + + .@"3008" => parsers.context_signal.parse(self, terminator_ch), + .@"6" => null, .@"66" => parsers.kitty_text_sizing.parse(self, terminator_ch), diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig index 764de28aa..a56b38e88 100644 --- a/src/terminal/osc/parsers.zig +++ b/src/terminal/osc/parsers.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub const change_window_icon = @import("parsers/change_window_icon.zig"); +pub const context_signal = @import("parsers/context_signal.zig"); pub const change_window_title = @import("parsers/change_window_title.zig"); pub const clipboard_operation = @import("parsers/clipboard_operation.zig"); pub const color = @import("parsers/color.zig"); diff --git a/src/terminal/osc/parsers/context_signal.zig b/src/terminal/osc/parsers/context_signal.zig new file mode 100644 index 000000000..40decb4e7 --- /dev/null +++ b/src/terminal/osc/parsers/context_signal.zig @@ -0,0 +1,587 @@ +//! OSC 3008: Hierarchical Context Signalling (UAPI spec) +//! Specification: https://uapi-group.org/specifications/specs/osc_context/ +//! +//! OSC 3008 allows programs to signal context changes to the terminal emulator. +//! Each context has an identifier and metadata fields. Contexts are hierarchical +//! and form a stack. + +const std = @import("std"); +const Parser = @import("../../osc.zig").Parser; +const OSCCommand = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_context_signal); + +/// Maximum length of a context identifier (per spec). +const max_context_id_len = 64; + +/// A single OSC 3008 context signal command. +pub const Command = struct { + action: Action, + /// The context identifier. Must be 1-64 characters in the 32..126 byte range. + id: []const u8, + /// Raw unparsed metadata fields after the context ID. + /// Fields are semicolon-separated key=value pairs. + /// Parsed lazily via `readField`. + fields_raw: []const u8, + + pub const Action = enum { + /// OSC 3008;start= — initiates, updates, or returns to a context. + start, + /// OSC 3008;end= — terminates a context. + end, + }; + + /// Read a metadata field value from the raw fields string. + /// Returns null if the field is not present or malformed. + pub fn readField( + self: Command, + comptime field: Field, + ) field.Type() { + return field.read(self.fields_raw); + } +}; + +/// Context types defined by the specification. +pub const ContextType = enum { + boot, + container, + vm, + elevate, + chpriv, + subcontext, + remote, + shell, + command, + app, + service, + session, + + pub fn parse(value: []const u8) ?ContextType { + const map = std.StaticStringMap(ContextType).initComptime(.{ + .{ "boot", .boot }, + .{ "container", .container }, + .{ "vm", .vm }, + .{ "elevate", .elevate }, + .{ "chpriv", .chpriv }, + .{ "subcontext", .subcontext }, + .{ "remote", .remote }, + .{ "shell", .shell }, + .{ "command", .command }, + .{ "app", .app }, + .{ "service", .service }, + .{ "session", .session }, + }); + return map.get(value); + } +}; + +/// Exit status for the `exit` end-sequence field. +pub const ExitStatus = enum { + success, + failure, + crash, + interrupt, + + pub fn parse(value: []const u8) ?ExitStatus { + const map = std.StaticStringMap(ExitStatus).initComptime(.{ + .{ "success", .success }, + .{ "failure", .failure }, + .{ "crash", .crash }, + .{ "interrupt", .interrupt }, + }); + return map.get(value); + } +}; + +/// Metadata fields that can appear in OSC 3008 sequences. +/// Fields are read lazily from the raw string using the `read` method. +pub const Field = enum { + // Start sequence fields + type, + user, + hostname, + machineid, + bootid, + pid, + pidfdid, + comm, + cwd, + cmdline, + vm, + container, + targetuser, + targethost, + sessionid, + + // End sequence fields + exit, + status, + signal, + + pub fn Type(comptime self: Field) type { + return switch (self) { + .type => ?ContextType, + .exit => ?ExitStatus, + .pid, .pidfdid => ?u64, + .status => ?u64, + // All other fields are string values + .user, + .hostname, + .machineid, + .bootid, + .comm, + .cwd, + .cmdline, + .vm, + .container, + .targetuser, + .targethost, + .sessionid, + .signal, + => ?[]const u8, + }; + } + + fn key(comptime self: Field) []const u8 { + return switch (self) { + .type => "type", + .user => "user", + .hostname => "hostname", + .machineid => "machineid", + .bootid => "bootid", + .pid => "pid", + .pidfdid => "pidfdid", + .comm => "comm", + .cwd => "cwd", + .cmdline => "cmdline", + .vm => "vm", + .container => "container", + .targetuser => "targetuser", + .targethost => "targethost", + .sessionid => "sessionid", + .exit => "exit", + .status => "status", + .signal => "signal", + }; + } + + /// Read the field value from the raw fields string. + /// + /// The raw fields string contains semicolon-separated key=value pairs + /// e.g. "type=container;user=lennart;hostname=zeta". + /// + /// Unknown or malformed fields are ignored per the specification. + pub fn read( + comptime self: Field, + raw: []const u8, + ) self.Type() { + var remaining = raw; + while (remaining.len > 0) { + const len = std.mem.indexOfScalar( + u8, + remaining, + ';', + ) orelse remaining.len; + + const full = remaining[0..len]; + + // Parse key=value + 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, advance past the semicolon + if (len < remaining.len) { + remaining = remaining[len + 1 ..]; + continue; + } + + break; + }; + + return switch (self) { + .type => ContextType.parse(value), + .exit => ExitStatus.parse(value), + .pid, .pidfdid, .status => std.fmt.parseInt( + u64, + value, + 10, + ) catch null, + // String fields + .user, + .hostname, + .machineid, + .bootid, + .comm, + .cwd, + .cmdline, + .vm, + .container, + .targetuser, + .targethost, + .sessionid, + .signal, + => if (value.len > 0) value else null, + }; + } + + // Not found + return null; + } +}; + +/// Parse OSC 3008: hierarchical context signalling. +/// +/// Expected data format (after "3008;" prefix has been consumed by the state machine): +/// start=[;=]* +/// end=[;=]* +pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 0) { + parser.state = .invalid; + return null; + } + + // Determine the action (start= or end=) + const action: Command.Action = action: { + if (std.mem.startsWith(u8, data, "start=")) break :action .start; + if (std.mem.startsWith(u8, data, "end=")) break :action .end; + + log.warn("OSC 3008: expected 'start=' or 'end=' prefix, got: {s}", .{ + data[0..@min(data.len, 10)], + }); + parser.state = .invalid; + return null; + }; + + // Skip past the "start=" or "end=" prefix + const prefix_len: usize = switch (action) { + .start => "start=".len, + .end => "end=".len, + }; + const rest = data[prefix_len..]; + + if (rest.len == 0) { + log.warn("OSC 3008: missing context ID", .{}); + parser.state = .invalid; + return null; + } + + // Extract the context ID (up to the first semicolon or end of data) + const id_end = std.mem.indexOfScalar(u8, rest, ';') orelse rest.len; + const id = rest[0..id_end]; + + // Validate context ID length (1-64 chars per spec) + if (id.len == 0 or id.len > max_context_id_len) { + log.warn("OSC 3008: context ID length {d} out of range (1-{d})", .{ + id.len, + max_context_id_len, + }); + parser.state = .invalid; + return null; + } + + // Validate context ID characters (32..126 byte range per spec) + for (id) |c| { + if (c < 0x20 or c > 0x7e) { + log.warn("OSC 3008: invalid character 0x{x:0>2} in context ID", .{c}); + parser.state = .invalid; + return null; + } + } + + // Extract raw metadata fields (everything after the ID) + const fields_raw = if (id_end < rest.len) rest[id_end + 1 ..] else ""; + + parser.command = .{ + .context_signal = .{ + .action = action, + .id = id, + .fields_raw = fields_raw, + }, + }; + + return &parser.command; +} + +// ============================================================================ +// Tests +// ============================================================================ + +test "OSC 3008: basic start command" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;start=abc123"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.action == .start); + try testing.expectEqualStrings("abc123", cmd.context_signal.id); + try testing.expectEqualStrings("", cmd.context_signal.fields_raw); +} + +test "OSC 3008: basic end command" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;end=abc123"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.action == .end); + try testing.expectEqualStrings("abc123", cmd.context_signal.id); + try testing.expectEqualStrings("", cmd.context_signal.fields_raw); +} + +test "OSC 3008: start with metadata fields" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;start=bed86fab93af4328bbed0a1224af6d40;type=container;user=lennart;hostname=zeta"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.action == .start); + try testing.expectEqualStrings("bed86fab93af4328bbed0a1224af6d40", cmd.context_signal.id); + + // Read individual fields + try testing.expect(cmd.context_signal.readField(.type).? == .container); + try testing.expectEqualStrings("lennart", cmd.context_signal.readField(.user).?); + try testing.expectEqualStrings("zeta", cmd.context_signal.readField(.hostname).?); +} + +test "OSC 3008: start with all common fields" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;start=myctx;type=shell;user=root;hostname=myhost;machineid=3deb5353d3ba43d08201c136a47ead7b;bootid=d4a3d0fdf2e24fdea6d971ce73f4fbf2;pid=1062862;pidfdid=1063162;comm=bash"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.readField(.type).? == .shell); + try testing.expectEqualStrings("root", cmd.context_signal.readField(.user).?); + try testing.expectEqualStrings("myhost", cmd.context_signal.readField(.hostname).?); + try testing.expectEqualStrings("3deb5353d3ba43d08201c136a47ead7b", cmd.context_signal.readField(.machineid).?); + try testing.expectEqualStrings("d4a3d0fdf2e24fdea6d971ce73f4fbf2", cmd.context_signal.readField(.bootid).?); + try testing.expectEqual(@as(u64, 1062862), cmd.context_signal.readField(.pid).?); + try testing.expectEqual(@as(u64, 1063162), cmd.context_signal.readField(.pidfdid).?); + try testing.expectEqualStrings("bash", cmd.context_signal.readField(.comm).?); +} + +test "OSC 3008: end with exit metadata" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;end=myctx;exit=success;status=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.action == .end); + try testing.expectEqualStrings("myctx", cmd.context_signal.id); + try testing.expect(cmd.context_signal.readField(.exit).? == .success); + try testing.expectEqual(@as(u64, 0), cmd.context_signal.readField(.status).?); +} + +test "OSC 3008: end with failure exit" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;end=myctx;exit=failure;status=1;signal=SIGKILL"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.readField(.exit).? == .failure); + try testing.expectEqual(@as(u64, 1), cmd.context_signal.readField(.status).?); + try testing.expectEqualStrings("SIGKILL", cmd.context_signal.readField(.signal).?); +} + +test "OSC 3008: unknown fields are ignored" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;start=myctx;type=shell;unknownfield=value;user=root"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.readField(.type).? == .shell); + try testing.expectEqualStrings("root", cmd.context_signal.readField(.user).?); +} + +test "OSC 3008: missing field returns null" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;start=myctx;user=lennart"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.readField(.type) == null); + try testing.expect(cmd.context_signal.readField(.hostname) == null); + try testing.expect(cmd.context_signal.readField(.pid) == null); +} + +test "OSC 3008: invalid prefix" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;bogus=abc123"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 3008: empty data" { + const testing = std.testing; + + // Can't really produce empty data after "3008;" because the state machine + // won't write a writer for that case, but we test the edge case where + // only "start=" is present with no ID. + var p: Parser = .init(null); + const input = "3008;start="; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 3008: max length context ID" { + const testing = std.testing; + + var p: Parser = .init(null); + const id = "a" ** 64; + const input = "3008;start=" ++ id; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expectEqualStrings(id, cmd.context_signal.id); +} + +test "OSC 3008: over-length context ID" { + const testing = std.testing; + + var p: Parser = .init(null); + const id = "a" ** 65; + const input = "3008;start=" ++ id; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 3008: context type enum coverage" { + const testing = std.testing; + + const types = [_]struct { str: []const u8, expected: ContextType }{ + .{ .str = "boot", .expected = .boot }, + .{ .str = "container", .expected = .container }, + .{ .str = "vm", .expected = .vm }, + .{ .str = "elevate", .expected = .elevate }, + .{ .str = "chpriv", .expected = .chpriv }, + .{ .str = "subcontext", .expected = .subcontext }, + .{ .str = "remote", .expected = .remote }, + .{ .str = "shell", .expected = .shell }, + .{ .str = "command", .expected = .command }, + .{ .str = "app", .expected = .app }, + .{ .str = "service", .expected = .service }, + .{ .str = "session", .expected = .session }, + }; + + for (types) |t| { + try testing.expectEqual(t.expected, ContextType.parse(t.str).?); + } + + try testing.expect(ContextType.parse("invalid") == null); +} + +test "OSC 3008: exit status enum coverage" { + const testing = std.testing; + + try testing.expect(ExitStatus.parse("success").? == .success); + try testing.expect(ExitStatus.parse("failure").? == .failure); + try testing.expect(ExitStatus.parse("crash").? == .crash); + try testing.expect(ExitStatus.parse("interrupt").? == .interrupt); + try testing.expect(ExitStatus.parse("invalid") == null); +} + +test "OSC 3008: spec example - container start" { + const testing = std.testing; + + // From the spec: a new container "foobar" invoked by user "lennart" on host "zeta" + var p: Parser = .init(null); + const input = "3008;start=bed86fab93af4328bbed0a1224af6d40;type=container;user=lennart;hostname=zeta;machineid=3deb5353d3ba43d08201c136a47ead7b;bootid=d4a3d0fdf2e24fdea6d971ce73f4fbf2;pid=1062862;pidfdid=1063162;comm=systemd-nspawn;container=foobar"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.action == .start); + try testing.expectEqualStrings("bed86fab93af4328bbed0a1224af6d40", cmd.context_signal.id); + try testing.expect(cmd.context_signal.readField(.type).? == .container); + try testing.expectEqualStrings("lennart", cmd.context_signal.readField(.user).?); + try testing.expectEqualStrings("zeta", cmd.context_signal.readField(.hostname).?); + try testing.expectEqualStrings("systemd-nspawn", cmd.context_signal.readField(.comm).?); + try testing.expectEqualStrings("foobar", cmd.context_signal.readField(.container).?); + try testing.expectEqual(@as(u64, 1062862), cmd.context_signal.readField(.pid).?); +} + +test "OSC 3008: spec example - context end" { + const testing = std.testing; + + // From the spec: context end + var p: Parser = .init(null); + const input = "3008;end=bed86fab93af4328bbed0a1224af6d40"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.action == .end); + try testing.expectEqualStrings("bed86fab93af4328bbed0a1224af6d40", cmd.context_signal.id); +} + +test "OSC 3008: cwd and cmdline fields" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;start=myctx;type=command;cwd=/home/user;cmdline=ls -la"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expectEqualStrings("/home/user", cmd.context_signal.readField(.cwd).?); + try testing.expectEqualStrings("ls -la", cmd.context_signal.readField(.cmdline).?); +} + +test "OSC 3008: start command with no fields" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "3008;start=simpleid"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .context_signal); + try testing.expect(cmd.context_signal.action == .start); + try testing.expectEqualStrings("simpleid", cmd.context_signal.id); + try testing.expect(cmd.context_signal.readField(.type) == null); + try testing.expect(cmd.context_signal.readField(.user) == null); + try testing.expect(cmd.context_signal.readField(.exit) == null); +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 60840d84b..df1074d27 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -2048,6 +2048,7 @@ pub fn Stream(comptime Handler: type) type { .conemu_run_process, .kitty_text_sizing, .kitty_clipboard_protocol, + .context_signal, => { log.debug("unimplemented OSC callback: {}", .{cmd}); }, -- 2.51.2 From 3e1004717b7af345dd73629153cbccb458b457fc Mon Sep 17 00:00:00 2001 From: Prakhar54-byte <162185166+Prakhar54-byte@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:20:42 +0530 Subject: [PATCH 2/3] refactor: apply PR feedback - Use `std.meta.stringToEnum` in ContextType and ExitStatus - Ensure `parseInt` only accepts digits for pids - Use `@tagName` for string representation in Field - Rename `fields_raw` to `metadata` - Rename `readField` to `readOption` --- src/terminal/osc/parsers/context_signal.zig | 144 ++++++++------------ 1 file changed, 55 insertions(+), 89 deletions(-) diff --git a/src/terminal/osc/parsers/context_signal.zig b/src/terminal/osc/parsers/context_signal.zig index 40decb4e7..f4a6efe77 100644 --- a/src/terminal/osc/parsers/context_signal.zig +++ b/src/terminal/osc/parsers/context_signal.zig @@ -21,8 +21,8 @@ pub const Command = struct { id: []const u8, /// Raw unparsed metadata fields after the context ID. /// Fields are semicolon-separated key=value pairs. - /// Parsed lazily via `readField`. - fields_raw: []const u8, + /// Parsed lazily via `readOption`. + metadata: []const u8, pub const Action = enum { /// OSC 3008;start= — initiates, updates, or returns to a context. @@ -33,11 +33,11 @@ pub const Command = struct { /// Read a metadata field value from the raw fields string. /// Returns null if the field is not present or malformed. - pub fn readField( + pub fn readOption( self: Command, - comptime field: Field, - ) field.Type() { - return field.read(self.fields_raw); + comptime option: Field, + ) option.Type() { + return option.read(self.metadata); } }; @@ -57,21 +57,7 @@ pub const ContextType = enum { session, pub fn parse(value: []const u8) ?ContextType { - const map = std.StaticStringMap(ContextType).initComptime(.{ - .{ "boot", .boot }, - .{ "container", .container }, - .{ "vm", .vm }, - .{ "elevate", .elevate }, - .{ "chpriv", .chpriv }, - .{ "subcontext", .subcontext }, - .{ "remote", .remote }, - .{ "shell", .shell }, - .{ "command", .command }, - .{ "app", .app }, - .{ "service", .service }, - .{ "session", .session }, - }); - return map.get(value); + return std.meta.stringToEnum(ContextType, value); } }; @@ -83,13 +69,7 @@ pub const ExitStatus = enum { interrupt, pub fn parse(value: []const u8) ?ExitStatus { - const map = std.StaticStringMap(ExitStatus).initComptime(.{ - .{ "success", .success }, - .{ "failure", .failure }, - .{ "crash", .crash }, - .{ "interrupt", .interrupt }, - }); - return map.get(value); + return std.meta.stringToEnum(ExitStatus, value); } }; @@ -143,26 +123,7 @@ pub const Field = enum { } fn key(comptime self: Field) []const u8 { - return switch (self) { - .type => "type", - .user => "user", - .hostname => "hostname", - .machineid => "machineid", - .bootid => "bootid", - .pid => "pid", - .pidfdid => "pidfdid", - .comm => "comm", - .cwd => "cwd", - .cmdline => "cmdline", - .vm => "vm", - .container => "container", - .targetuser => "targetuser", - .targethost => "targethost", - .sessionid => "sessionid", - .exit => "exit", - .status => "status", - .signal => "signal", - }; + return @tagName(self); } /// Read the field value from the raw fields string. @@ -213,11 +174,16 @@ pub const Field = enum { return switch (self) { .type => ContextType.parse(value), .exit => ExitStatus.parse(value), - .pid, .pidfdid, .status => std.fmt.parseInt( - u64, - value, - 10, - ) catch null, + .pid, .pidfdid, .status => value: { + for (value) |c| { + if (c < '0' or c > '9') break :value null; + } + break :value std.fmt.parseInt( + u64, + value, + 10, + ) catch null; + }, // String fields .user, .hostname, @@ -306,13 +272,13 @@ pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { } // Extract raw metadata fields (everything after the ID) - const fields_raw = if (id_end < rest.len) rest[id_end + 1 ..] else ""; + const metadata = if (id_end < rest.len) rest[id_end + 1 ..] else ""; parser.command = .{ .context_signal = .{ .action = action, .id = id, - .fields_raw = fields_raw, + .metadata = metadata, }, }; @@ -334,7 +300,7 @@ test "OSC 3008: basic start command" { try testing.expect(cmd == .context_signal); try testing.expect(cmd.context_signal.action == .start); try testing.expectEqualStrings("abc123", cmd.context_signal.id); - try testing.expectEqualStrings("", cmd.context_signal.fields_raw); + try testing.expectEqualStrings("", cmd.context_signal.metadata); } test "OSC 3008: basic end command" { @@ -348,7 +314,7 @@ test "OSC 3008: basic end command" { try testing.expect(cmd == .context_signal); try testing.expect(cmd.context_signal.action == .end); try testing.expectEqualStrings("abc123", cmd.context_signal.id); - try testing.expectEqualStrings("", cmd.context_signal.fields_raw); + try testing.expectEqualStrings("", cmd.context_signal.metadata); } test "OSC 3008: start with metadata fields" { @@ -364,9 +330,9 @@ test "OSC 3008: start with metadata fields" { try testing.expectEqualStrings("bed86fab93af4328bbed0a1224af6d40", cmd.context_signal.id); // Read individual fields - try testing.expect(cmd.context_signal.readField(.type).? == .container); - try testing.expectEqualStrings("lennart", cmd.context_signal.readField(.user).?); - try testing.expectEqualStrings("zeta", cmd.context_signal.readField(.hostname).?); + try testing.expect(cmd.context_signal.readOption(.type).? == .container); + try testing.expectEqualStrings("lennart", cmd.context_signal.readOption(.user).?); + try testing.expectEqualStrings("zeta", cmd.context_signal.readOption(.hostname).?); } test "OSC 3008: start with all common fields" { @@ -378,14 +344,14 @@ test "OSC 3008: start with all common fields" { const cmd = p.end(null).?.*; try testing.expect(cmd == .context_signal); - try testing.expect(cmd.context_signal.readField(.type).? == .shell); - try testing.expectEqualStrings("root", cmd.context_signal.readField(.user).?); - try testing.expectEqualStrings("myhost", cmd.context_signal.readField(.hostname).?); - try testing.expectEqualStrings("3deb5353d3ba43d08201c136a47ead7b", cmd.context_signal.readField(.machineid).?); - try testing.expectEqualStrings("d4a3d0fdf2e24fdea6d971ce73f4fbf2", cmd.context_signal.readField(.bootid).?); - try testing.expectEqual(@as(u64, 1062862), cmd.context_signal.readField(.pid).?); - try testing.expectEqual(@as(u64, 1063162), cmd.context_signal.readField(.pidfdid).?); - try testing.expectEqualStrings("bash", cmd.context_signal.readField(.comm).?); + try testing.expect(cmd.context_signal.readOption(.type).? == .shell); + try testing.expectEqualStrings("root", cmd.context_signal.readOption(.user).?); + try testing.expectEqualStrings("myhost", cmd.context_signal.readOption(.hostname).?); + try testing.expectEqualStrings("3deb5353d3ba43d08201c136a47ead7b", cmd.context_signal.readOption(.machineid).?); + try testing.expectEqualStrings("d4a3d0fdf2e24fdea6d971ce73f4fbf2", cmd.context_signal.readOption(.bootid).?); + try testing.expectEqual(@as(u64, 1062862), cmd.context_signal.readOption(.pid).?); + try testing.expectEqual(@as(u64, 1063162), cmd.context_signal.readOption(.pidfdid).?); + try testing.expectEqualStrings("bash", cmd.context_signal.readOption(.comm).?); } test "OSC 3008: end with exit metadata" { @@ -399,8 +365,8 @@ test "OSC 3008: end with exit metadata" { try testing.expect(cmd == .context_signal); try testing.expect(cmd.context_signal.action == .end); try testing.expectEqualStrings("myctx", cmd.context_signal.id); - try testing.expect(cmd.context_signal.readField(.exit).? == .success); - try testing.expectEqual(@as(u64, 0), cmd.context_signal.readField(.status).?); + try testing.expect(cmd.context_signal.readOption(.exit).? == .success); + try testing.expectEqual(@as(u64, 0), cmd.context_signal.readOption(.status).?); } test "OSC 3008: end with failure exit" { @@ -412,9 +378,9 @@ test "OSC 3008: end with failure exit" { const cmd = p.end(null).?.*; try testing.expect(cmd == .context_signal); - try testing.expect(cmd.context_signal.readField(.exit).? == .failure); - try testing.expectEqual(@as(u64, 1), cmd.context_signal.readField(.status).?); - try testing.expectEqualStrings("SIGKILL", cmd.context_signal.readField(.signal).?); + try testing.expect(cmd.context_signal.readOption(.exit).? == .failure); + try testing.expectEqual(@as(u64, 1), cmd.context_signal.readOption(.status).?); + try testing.expectEqualStrings("SIGKILL", cmd.context_signal.readOption(.signal).?); } test "OSC 3008: unknown fields are ignored" { @@ -426,8 +392,8 @@ test "OSC 3008: unknown fields are ignored" { const cmd = p.end(null).?.*; try testing.expect(cmd == .context_signal); - try testing.expect(cmd.context_signal.readField(.type).? == .shell); - try testing.expectEqualStrings("root", cmd.context_signal.readField(.user).?); + try testing.expect(cmd.context_signal.readOption(.type).? == .shell); + try testing.expectEqualStrings("root", cmd.context_signal.readOption(.user).?); } test "OSC 3008: missing field returns null" { @@ -439,9 +405,9 @@ test "OSC 3008: missing field returns null" { const cmd = p.end(null).?.*; try testing.expect(cmd == .context_signal); - try testing.expect(cmd.context_signal.readField(.type) == null); - try testing.expect(cmd.context_signal.readField(.hostname) == null); - try testing.expect(cmd.context_signal.readField(.pid) == null); + try testing.expect(cmd.context_signal.readOption(.type) == null); + try testing.expect(cmd.context_signal.readOption(.hostname) == null); + try testing.expect(cmd.context_signal.readOption(.pid) == null); } test "OSC 3008: invalid prefix" { @@ -535,12 +501,12 @@ test "OSC 3008: spec example - container start" { try testing.expect(cmd == .context_signal); try testing.expect(cmd.context_signal.action == .start); try testing.expectEqualStrings("bed86fab93af4328bbed0a1224af6d40", cmd.context_signal.id); - try testing.expect(cmd.context_signal.readField(.type).? == .container); - try testing.expectEqualStrings("lennart", cmd.context_signal.readField(.user).?); - try testing.expectEqualStrings("zeta", cmd.context_signal.readField(.hostname).?); - try testing.expectEqualStrings("systemd-nspawn", cmd.context_signal.readField(.comm).?); - try testing.expectEqualStrings("foobar", cmd.context_signal.readField(.container).?); - try testing.expectEqual(@as(u64, 1062862), cmd.context_signal.readField(.pid).?); + try testing.expect(cmd.context_signal.readOption(.type).? == .container); + try testing.expectEqualStrings("lennart", cmd.context_signal.readOption(.user).?); + try testing.expectEqualStrings("zeta", cmd.context_signal.readOption(.hostname).?); + try testing.expectEqualStrings("systemd-nspawn", cmd.context_signal.readOption(.comm).?); + try testing.expectEqualStrings("foobar", cmd.context_signal.readOption(.container).?); + try testing.expectEqual(@as(u64, 1062862), cmd.context_signal.readOption(.pid).?); } test "OSC 3008: spec example - context end" { @@ -566,8 +532,8 @@ test "OSC 3008: cwd and cmdline fields" { const cmd = p.end(null).?.*; try testing.expect(cmd == .context_signal); - try testing.expectEqualStrings("/home/user", cmd.context_signal.readField(.cwd).?); - try testing.expectEqualStrings("ls -la", cmd.context_signal.readField(.cmdline).?); + try testing.expectEqualStrings("/home/user", cmd.context_signal.readOption(.cwd).?); + try testing.expectEqualStrings("ls -la", cmd.context_signal.readOption(.cmdline).?); } test "OSC 3008: start command with no fields" { @@ -581,7 +547,7 @@ test "OSC 3008: start command with no fields" { try testing.expect(cmd == .context_signal); try testing.expect(cmd.context_signal.action == .start); try testing.expectEqualStrings("simpleid", cmd.context_signal.id); - try testing.expect(cmd.context_signal.readField(.type) == null); - try testing.expect(cmd.context_signal.readField(.user) == null); - try testing.expect(cmd.context_signal.readField(.exit) == null); + try testing.expect(cmd.context_signal.readOption(.type) == null); + try testing.expect(cmd.context_signal.readOption(.user) == null); + try testing.expect(cmd.context_signal.readOption(.exit) == null); } -- 2.51.2 From eafdbaaadad375f62e2a8e825ca9fe931fb2708e Mon Sep 17 00:00:00 2001 From: Prakhar54-byte <162185166+Prakhar54-byte@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:58:08 +0530 Subject: [PATCH 3/3] refactor: simplify Enum parse call and loop parsing logic --- src/terminal/osc/parsers/context_signal.zig | 24 +++++---------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/terminal/osc/parsers/context_signal.zig b/src/terminal/osc/parsers/context_signal.zig index f4a6efe77..ff82af392 100644 --- a/src/terminal/osc/parsers/context_signal.zig +++ b/src/terminal/osc/parsers/context_signal.zig @@ -136,16 +136,8 @@ pub const Field = enum { comptime self: Field, raw: []const u8, ) self.Type() { - var remaining = raw; - while (remaining.len > 0) { - const len = std.mem.indexOfScalar( - u8, - remaining, - ';', - ) orelse remaining.len; - - const full = remaining[0..len]; - + var it = std.mem.splitScalar(u8, raw, ';'); + while (it.next()) |full| { // Parse key=value const value = value: { if (std.mem.indexOfScalar( @@ -162,18 +154,12 @@ pub const Field = enum { } } - // No match, advance past the semicolon - if (len < remaining.len) { - remaining = remaining[len + 1 ..]; - continue; - } - - break; + continue; }; return switch (self) { - .type => ContextType.parse(value), - .exit => ExitStatus.parse(value), + .type => .parse(value), + .exit => .parse(value), .pid, .pidfdid, .status => value: { for (value) |c| { if (c < '0' or c > '9') break :value null;