mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
terminal: OSC parser
This commit is contained in:
@ -7,6 +7,7 @@ const Parser = @This();
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const table = @import("parse_table.zig").table;
|
const table = @import("parse_table.zig").table;
|
||||||
|
const osc = @import("osc.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.parser);
|
const log = std.log.scoped(.parser);
|
||||||
|
|
||||||
@ -66,6 +67,9 @@ pub const Action = union(enum) {
|
|||||||
/// Execute the ESC command.
|
/// Execute the ESC command.
|
||||||
esc_dispatch: ESC,
|
esc_dispatch: ESC,
|
||||||
|
|
||||||
|
/// Execute the OSC command.
|
||||||
|
osc_dispatch: osc.Command,
|
||||||
|
|
||||||
pub const CSI = struct {
|
pub const CSI = struct {
|
||||||
intermediates: []u8,
|
intermediates: []u8,
|
||||||
params: []u16,
|
params: []u16,
|
||||||
@ -95,6 +99,9 @@ params_idx: u8 = 0,
|
|||||||
param_acc: u16 = 0,
|
param_acc: u16 = 0,
|
||||||
param_acc_idx: u8 = 0,
|
param_acc_idx: u8 = 0,
|
||||||
|
|
||||||
|
/// Parser for OSC sequences
|
||||||
|
osc_parser: osc.Parser = .{},
|
||||||
|
|
||||||
pub fn init() Parser {
|
pub fn init() Parser {
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
@ -126,20 +133,28 @@ pub fn next(self: *Parser, c: u8) [3]?Action {
|
|||||||
// 2. transition action
|
// 2. transition action
|
||||||
// 3. entry action to new state
|
// 3. entry action to new state
|
||||||
return [3]?Action{
|
return [3]?Action{
|
||||||
switch (self.state) {
|
// Exit depends on current state
|
||||||
.osc_string => @panic("TODO"), // TODO: osc_end
|
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
|
.dcs_passthrough => @panic("TODO"), // TODO: unhook
|
||||||
else => null,
|
else => null,
|
||||||
},
|
},
|
||||||
|
|
||||||
self.doAction(action, c),
|
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: {
|
.escape, .dcs_entry, .csi_entry => clear: {
|
||||||
self.clear();
|
self.clear();
|
||||||
break :clear null;
|
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
|
.dcs_passthrough => @panic("TODO"), // TODO: hook
|
||||||
else => null,
|
else => null,
|
||||||
},
|
},
|
||||||
@ -147,7 +162,6 @@ pub fn next(self: *Parser, c: u8) [3]?Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
|
fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
|
||||||
_ = self;
|
|
||||||
return switch (action) {
|
return switch (action) {
|
||||||
.none, .ignore => null,
|
.none, .ignore => null,
|
||||||
.print => Action{ .print = c },
|
.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.
|
// The client is expected to perform no action.
|
||||||
break :param null;
|
break :param null;
|
||||||
},
|
},
|
||||||
|
.osc_put => osc_put: {
|
||||||
|
self.osc_parser.next(c);
|
||||||
|
break :osc_put null;
|
||||||
|
},
|
||||||
.csi_dispatch => csi_dispatch: {
|
.csi_dispatch => csi_dispatch: {
|
||||||
// Finalize parameters if we have one
|
// Finalize parameters if we have one
|
||||||
if (self.param_acc_idx > 0) {
|
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]);
|
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),
|
.execute => |code| try self.execute(alloc, code),
|
||||||
.csi_dispatch => |csi| try self.csiDispatch(alloc, csi),
|
.csi_dispatch => |csi| try self.csiDispatch(alloc, csi),
|
||||||
.esc_dispatch => |esc| try self.escDispatch(alloc, esc),
|
.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 {
|
test {
|
||||||
|
_ = @import("osc.zig");
|
||||||
_ = Parser;
|
_ = Parser;
|
||||||
_ = Tabstops;
|
_ = 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
|
// events
|
||||||
single(&result, 0x19, source, source, .ignore);
|
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, 0x1C, 0x1F, source, source, .ignore);
|
||||||
range(&result, 0x20, 0x7F, source, source, .osc_put);
|
range(&result, 0x20, 0x7F, source, source, .osc_put);
|
||||||
|
|
||||||
// => ground
|
// => ground
|
||||||
|
single(&result, 0x07, source, .ground, .none);
|
||||||
single(&result, 0x9C, source, .ground, .none);
|
single(&result, 0x9C, source, .ground, .none);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user