mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
terminal: OSC parser
This commit is contained in:
@ -7,6 +7,7 @@ const Parser = @This();
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const table = @import("parse_table.zig").table;
|
||||
const osc = @import("osc.zig");
|
||||
|
||||
const log = std.log.scoped(.parser);
|
||||
|
||||
@ -66,6 +67,9 @@ pub const Action = union(enum) {
|
||||
/// Execute the ESC command.
|
||||
esc_dispatch: ESC,
|
||||
|
||||
/// Execute the OSC command.
|
||||
osc_dispatch: osc.Command,
|
||||
|
||||
pub const CSI = struct {
|
||||
intermediates: []u8,
|
||||
params: []u16,
|
||||
@ -95,6 +99,9 @@ params_idx: u8 = 0,
|
||||
param_acc: u16 = 0,
|
||||
param_acc_idx: u8 = 0,
|
||||
|
||||
/// Parser for OSC sequences
|
||||
osc_parser: osc.Parser = .{},
|
||||
|
||||
pub fn init() Parser {
|
||||
return .{};
|
||||
}
|
||||
@ -126,20 +133,28 @@ pub fn next(self: *Parser, c: u8) [3]?Action {
|
||||
// 2. transition action
|
||||
// 3. entry action to new state
|
||||
return [3]?Action{
|
||||
switch (self.state) {
|
||||
.osc_string => @panic("TODO"), // TODO: osc_end
|
||||
// Exit depends on current state
|
||||
if (self.state == next_state) null else switch (self.state) {
|
||||
.osc_string => if (self.osc_parser.end()) |cmd|
|
||||
Action{ .osc_dispatch = cmd }
|
||||
else
|
||||
null,
|
||||
.dcs_passthrough => @panic("TODO"), // TODO: unhook
|
||||
else => null,
|
||||
},
|
||||
|
||||
self.doAction(action, c),
|
||||
|
||||
switch (next_state) {
|
||||
// Entry depends on new state
|
||||
if (self.state == next_state) null else switch (next_state) {
|
||||
.escape, .dcs_entry, .csi_entry => clear: {
|
||||
self.clear();
|
||||
break :clear null;
|
||||
},
|
||||
.osc_string => @panic("TODO"), // TODO: osc_start
|
||||
.osc_string => osc_string: {
|
||||
self.osc_parser.reset();
|
||||
break :osc_string null;
|
||||
},
|
||||
.dcs_passthrough => @panic("TODO"), // TODO: hook
|
||||
else => null,
|
||||
},
|
||||
@ -147,7 +162,6 @@ pub fn next(self: *Parser, c: u8) [3]?Action {
|
||||
}
|
||||
|
||||
fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
|
||||
_ = self;
|
||||
return switch (action) {
|
||||
.none, .ignore => null,
|
||||
.print => Action{ .print = c },
|
||||
@ -191,6 +205,10 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
|
||||
// The client is expected to perform no action.
|
||||
break :param null;
|
||||
},
|
||||
.osc_put => osc_put: {
|
||||
self.osc_parser.next(c);
|
||||
break :osc_put null;
|
||||
},
|
||||
.csi_dispatch => csi_dispatch: {
|
||||
// Finalize parameters if we have one
|
||||
if (self.param_acc_idx > 0) {
|
||||
@ -309,3 +327,25 @@ test "csi: ESC [ 1 ; 4 H" {
|
||||
try testing.expectEqual(@as(u16, 4), d.params[1]);
|
||||
}
|
||||
}
|
||||
|
||||
test "osc: change window title" {
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
_ = p.next(']');
|
||||
_ = p.next('0');
|
||||
_ = p.next(';');
|
||||
_ = p.next('a');
|
||||
_ = p.next('b');
|
||||
_ = p.next('c');
|
||||
|
||||
{
|
||||
const a = p.next(0x07); // BEL
|
||||
try testing.expect(p.state == .ground);
|
||||
try testing.expect(a[0].? == .osc_dispatch);
|
||||
try testing.expect(a[1] == null);
|
||||
try testing.expect(a[2] == null);
|
||||
|
||||
const cmd = a[0].?.osc_dispatch;
|
||||
try testing.expect(cmd == .change_window_title);
|
||||
}
|
||||
}
|
||||
|
@ -137,6 +137,7 @@ pub fn appendChar(self: *Terminal, alloc: Allocator, c: u8) !void {
|
||||
.execute => |code| try self.execute(alloc, code),
|
||||
.csi_dispatch => |csi| try self.csiDispatch(alloc, csi),
|
||||
.esc_dispatch => |esc| try self.escDispatch(alloc, esc),
|
||||
.osc_dispatch => |cmd| log.warn("unhandled OSC: {}", .{cmd}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -626,6 +627,7 @@ fn getOrPutCell(self: *Terminal, alloc: Allocator, x: usize, y: usize) !*Cell {
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("osc.zig");
|
||||
_ = Parser;
|
||||
_ = Tabstops;
|
||||
}
|
||||
|
119
src/terminal/osc.zig
Normal file
119
src/terminal/osc.zig
Normal file
@ -0,0 +1,119 @@
|
||||
//! OSC (Operating System Command) related functions and types. OSC is
|
||||
//! another set of control sequences for terminal programs that start with
|
||||
//! "ESC ]". Unlike CSI or standard ESC sequences, they may contain strings
|
||||
//! and other irregular formatting so a dedicated parser is created to handle it.
|
||||
const osc = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = std.log.scoped(.osc);
|
||||
|
||||
pub const Command = union(enum) {
|
||||
/// Set the window title of the terminal
|
||||
///
|
||||
/// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8
|
||||
/// with each code unit further encoded with two hex digets).
|
||||
///
|
||||
/// If title mode 2 is set or the terminal is setup for unconditional
|
||||
/// utf-8 titles text is interpreted as utf-8. Else text is interpreted
|
||||
/// as latin1.
|
||||
change_window_title: []const u8,
|
||||
};
|
||||
|
||||
pub const Parser = struct {
|
||||
state: State = .empty,
|
||||
command: Command = undefined,
|
||||
param_str: ?*[]const u8 = null,
|
||||
buf: [MAX_BUF]u8 = undefined,
|
||||
buf_start: usize = 0,
|
||||
buf_idx: usize = 0,
|
||||
complete: bool = false,
|
||||
|
||||
// Maximum length of a single OSC command. This is the full OSC command
|
||||
// sequence length (excluding ESC ]). This is arbitrary, I couldn't find
|
||||
// any definitive resource on how long this should be.
|
||||
const MAX_BUF = 2048;
|
||||
|
||||
pub const State = enum {
|
||||
empty,
|
||||
invalid,
|
||||
@"0",
|
||||
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;
|
||||
}
|
||||
|
||||
/// Consume the next character c and advance the parser state.
|
||||
pub fn next(self: *Parser, c: u8) void {
|
||||
// We store everything in the buffer so we can do a better job
|
||||
// logging if we get to an invalid command.
|
||||
self.buf[self.buf_idx] = c;
|
||||
self.buf_idx += 1;
|
||||
|
||||
log.info("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 => {},
|
||||
|
||||
.empty => switch (c) {
|
||||
'0' => self.state = .@"0",
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"0" => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .change_window_title = undefined };
|
||||
|
||||
self.state = .string;
|
||||
self.param_str = &self.command.change_window_title;
|
||||
self.buf_start = self.buf_idx;
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.string => {
|
||||
// Complete once we receive one character since we have
|
||||
// at least SOME value for the expected string value.
|
||||
self.complete = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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];
|
||||
}
|
||||
|
||||
return self.command;
|
||||
}
|
||||
};
|
||||
|
||||
test "OSC: change_window_title" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
p.next('0');
|
||||
p.next(';');
|
||||
p.next('a');
|
||||
p.next('b');
|
||||
const cmd = p.end().?;
|
||||
try testing.expect(cmd == .change_window_title);
|
||||
try testing.expectEqualStrings("ab", cmd.change_window_title);
|
||||
}
|
@ -319,11 +319,13 @@ fn genTable() Table {
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .ignore);
|
||||
range(&result, 0, 0x17, source, source, .ignore);
|
||||
range(&result, 0, 0x06, source, source, .ignore);
|
||||
range(&result, 0x08, 0x17, source, source, .ignore);
|
||||
range(&result, 0x1C, 0x1F, source, source, .ignore);
|
||||
range(&result, 0x20, 0x7F, source, source, .osc_put);
|
||||
|
||||
// => ground
|
||||
single(&result, 0x07, source, .ground, .none);
|
||||
single(&result, 0x9C, source, .ground, .none);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user