diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 826d38a92..a651c4d40 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -5,6 +5,7 @@ const osc = @This(); const std = @import("std"); +const mem = std.mem; const log = std.log.scoped(.osc); @@ -18,15 +19,36 @@ pub const Command = union(enum) { /// utf-8 titles text is interpreted as utf-8. Else text is interpreted /// as latin1. change_window_title: []const u8, + + /// First do a fresh-line. Then start a new command, and enter prompt mode: + /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a + /// prompt string (as if followed by OSC 133;P;k=i\007). Note: I've noticed + /// not all shells will send the prompt end code. + prompt_start: struct { + aid: ?[]const u8 = null, + }, }; pub const Parser = struct { + /// Current state of the parser. state: State = .empty, + + /// Current command of the parser, this accumulates. command: Command = undefined, - param_str: ?*[]const u8 = null, + + /// Current string parameter being populated (if non-null). + param_str: *[]const u8 = undefined, + + /// Buffer that stores the input we see for a single OSC command. + /// Slices in Command are offsets into this buffer. buf: [MAX_BUF]u8 = undefined, buf_start: usize = 0, buf_idx: usize = 0, + + /// Temporary state for key/value pairs + key: []const u8 = undefined, + + /// True when a command is complete/valid to return. complete: bool = false, // Maximum length of a single OSC command. This is the full OSC command @@ -37,14 +59,30 @@ pub const Parser = struct { pub const State = enum { empty, invalid, + + // Command prefixes. We could just accumulate and compare (mem.eql) + // but the state space is small enough that we just build it up this way. @"0", + @"1", + @"13", + @"133", + + // We're in a semantic prompt OSC command but we aren't sure + // what the command is yet, i.e. `133;` + semantic_prompt, + + semantic_option_start, + semantic_option_key, + semantic_option_value, + + // Expect a string parameter. param_str must be set as well as + // buf_start. string, }; /// Reset the parser start. pub fn reset(self: *Parser) void { self.state = .empty; - self.param_str = null; self.buf_start = 0; self.buf_idx = 0; self.complete = false; @@ -57,15 +95,16 @@ pub const Parser = struct { self.buf[self.buf_idx] = c; self.buf_idx += 1; - log.info("state = {} c = {x}", .{ self.state, c }); + // log.warn("state = {} c = {x}", .{ self.state, c }); switch (self.state) { - // Ignore, we're in some invalid state and we can't possibly - // do anything reasonable. - .invalid => {}, + // If we get something during the invalid state, we've + // ruined our entry. + .invalid => self.complete = false, .empty => switch (c) { '0' => self.state = .@"0", + '1' => self.state = .@"1", else => self.state = .invalid, }, @@ -80,6 +119,56 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"1" => switch (c) { + '3' => self.state = .@"13", + else => self.state = .invalid, + }, + + .@"13" => switch (c) { + '3' => self.state = .@"133", + else => self.state = .invalid, + }, + + .@"133" => switch (c) { + ';' => self.state = .semantic_prompt, + else => self.state = .invalid, + }, + + .semantic_prompt => switch (c) { + 'A' => { + self.state = .semantic_option_start; + self.command = .{ .prompt_start = .{} }; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .semantic_option_start => switch (c) { + ';' => { + self.state = .semantic_option_key; + self.buf_start = self.buf_idx; + }, + else => self.state = .invalid, + }, + + .semantic_option_key => switch (c) { + '=' => { + self.key = self.buf[self.buf_start .. self.buf_idx - 1]; + self.state = .semantic_option_value; + self.buf_start = self.buf_idx; + }, + else => {}, + }, + + .semantic_option_value => switch (c) { + ';' => { + self.endSemanticOptionValue(); + self.state = .semantic_option_key; + self.buf_start = self.buf_idx; + }, + else => {}, + }, + .string => { // Complete once we receive one character since we have // at least SOME value for the expected string value. @@ -88,17 +177,30 @@ pub const Parser = struct { } } + fn endSemanticOptionValue(self: *Parser) void { + const value = self.buf[self.buf_start..self.buf_idx]; + + if (mem.eql(u8, self.key, "aid")) { + switch (self.command) { + .prompt_start => |*v| v.aid = value, + else => {}, + } + } else log.info("unknown semantic prompts option: {s}", .{self.key}); + } + /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. - pub fn end(self: Parser) ?Command { + pub fn end(self: *Parser) ?Command { if (!self.complete) { log.warn("invalid OSC command: {s}", .{self.buf[0..self.buf_idx]}); return null; } - // If we have an expected string parameter, fill it in. - if (self.param_str) |param_str| { - param_str.* = self.buf[self.buf_start..self.buf_idx]; + // Other cleanup we may have to do depending on state. + switch (self.state) { + .semantic_option_value => self.endSemanticOptionValue(), + .string => self.param_str.* = self.buf[self.buf_start..self.buf_idx], + else => {}, } return self.command; @@ -117,3 +219,29 @@ test "OSC: change_window_title" { try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } + +test "OSC: prompt_start" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "133;A"; + for (input) |ch| p.next(ch); + + const cmd = p.end().?; + try testing.expect(cmd == .prompt_start); + try testing.expect(cmd.prompt_start.aid == null); +} + +test "OSC: prompt_start with single option" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "133;A;aid=14"; + for (input) |ch| p.next(ch); + + const cmd = p.end().?; + try testing.expect(cmd == .prompt_start); + try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); +}