mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
OSC parses prompt_start
This commit is contained in:
@ -5,6 +5,7 @@
|
|||||||
const osc = @This();
|
const osc = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
|
||||||
const log = std.log.scoped(.osc);
|
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
|
/// utf-8 titles text is interpreted as utf-8. Else text is interpreted
|
||||||
/// as latin1.
|
/// as latin1.
|
||||||
change_window_title: []const u8,
|
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 {
|
pub const Parser = struct {
|
||||||
|
/// Current state of the parser.
|
||||||
state: State = .empty,
|
state: State = .empty,
|
||||||
|
|
||||||
|
/// Current command of the parser, this accumulates.
|
||||||
command: Command = undefined,
|
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: [MAX_BUF]u8 = undefined,
|
||||||
buf_start: usize = 0,
|
buf_start: usize = 0,
|
||||||
buf_idx: 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,
|
complete: bool = false,
|
||||||
|
|
||||||
// Maximum length of a single OSC command. This is the full OSC command
|
// 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 {
|
pub const State = enum {
|
||||||
empty,
|
empty,
|
||||||
invalid,
|
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",
|
@"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,
|
string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Reset the parser start.
|
/// Reset the parser start.
|
||||||
pub fn reset(self: *Parser) void {
|
pub fn reset(self: *Parser) void {
|
||||||
self.state = .empty;
|
self.state = .empty;
|
||||||
self.param_str = null;
|
|
||||||
self.buf_start = 0;
|
self.buf_start = 0;
|
||||||
self.buf_idx = 0;
|
self.buf_idx = 0;
|
||||||
self.complete = false;
|
self.complete = false;
|
||||||
@ -57,15 +95,16 @@ pub const Parser = struct {
|
|||||||
self.buf[self.buf_idx] = c;
|
self.buf[self.buf_idx] = c;
|
||||||
self.buf_idx += 1;
|
self.buf_idx += 1;
|
||||||
|
|
||||||
log.info("state = {} c = {x}", .{ self.state, c });
|
// log.warn("state = {} c = {x}", .{ self.state, c });
|
||||||
|
|
||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
// Ignore, we're in some invalid state and we can't possibly
|
// If we get something during the invalid state, we've
|
||||||
// do anything reasonable.
|
// ruined our entry.
|
||||||
.invalid => {},
|
.invalid => self.complete = false,
|
||||||
|
|
||||||
.empty => switch (c) {
|
.empty => switch (c) {
|
||||||
'0' => self.state = .@"0",
|
'0' => self.state = .@"0",
|
||||||
|
'1' => self.state = .@"1",
|
||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -80,6 +119,56 @@ pub const Parser = struct {
|
|||||||
else => self.state = .invalid,
|
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 => {
|
.string => {
|
||||||
// Complete once we receive one character since we have
|
// Complete once we receive one character since we have
|
||||||
// at least SOME value for the expected string value.
|
// 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
|
/// End the sequence and return the command, if any. If the return value
|
||||||
/// is null, then no valid command was found.
|
/// is null, then no valid command was found.
|
||||||
pub fn end(self: Parser) ?Command {
|
pub fn end(self: *Parser) ?Command {
|
||||||
if (!self.complete) {
|
if (!self.complete) {
|
||||||
log.warn("invalid OSC command: {s}", .{self.buf[0..self.buf_idx]});
|
log.warn("invalid OSC command: {s}", .{self.buf[0..self.buf_idx]});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have an expected string parameter, fill it in.
|
// Other cleanup we may have to do depending on state.
|
||||||
if (self.param_str) |param_str| {
|
switch (self.state) {
|
||||||
param_str.* = self.buf[self.buf_start..self.buf_idx];
|
.semantic_option_value => self.endSemanticOptionValue(),
|
||||||
|
.string => self.param_str.* = self.buf[self.buf_start..self.buf_idx],
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.command;
|
return self.command;
|
||||||
@ -117,3 +219,29 @@ test "OSC: change_window_title" {
|
|||||||
try testing.expect(cmd == .change_window_title);
|
try testing.expect(cmd == .change_window_title);
|
||||||
try testing.expectEqualStrings("ab", 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.?);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user