terminal2: start Selection

This commit is contained in:
Mitchell Hashimoto
2024-03-05 19:44:58 -08:00
parent 4055f8af76
commit 0f5841baca
14 changed files with 5625 additions and 1 deletions

View File

@ -759,6 +759,7 @@ test "Selection: within" {
}
}
// X
test "Selection: order, standard" {
const testing = std.testing;
{
@ -808,6 +809,7 @@ test "Selection: order, standard" {
}
}
// X
test "Selection: order, rectangle" {
const testing = std.testing;
// Conventions:

View File

@ -1811,6 +1811,30 @@ pub const Pin = struct {
return false;
}
/// Returns true if self is before other. This is very expensive since
/// it requires traversing the linked list of pages. This should not
/// be called in performance critical paths.
pub fn isBefore(self: Pin, other: Pin) bool {
if (self.page == other.page) {
if (self.y < other.y) return true;
if (self.y > other.y) return false;
return self.x < other.x;
}
var page = self.page.next;
while (page) |p| : (page = p.next) {
if (p == other.page) return true;
}
return false;
}
pub fn eql(self: Pin, other: Pin) bool {
return self.page == other.page and
self.y == other.y and
self.x == other.x;
}
/// Move the pin down a certain number of rows, or return null if
/// the pin goes beyond the end of the screen.
pub fn down(self: Pin, n: usize) ?Pin {

794
src/terminal2/Parser.zig Normal file
View File

@ -0,0 +1,794 @@
//! VT-series parser for escape and control sequences.
//!
//! This is implemented directly as the state machine described on
//! vt100.net: https://vt100.net/emu/dec_ansi_parser
const Parser = @This();
const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
const table = @import("parse_table.zig").table;
const osc = @import("osc.zig");
const log = std.log.scoped(.parser);
/// States for the state machine
pub const State = enum {
ground,
escape,
escape_intermediate,
csi_entry,
csi_intermediate,
csi_param,
csi_ignore,
dcs_entry,
dcs_param,
dcs_intermediate,
dcs_passthrough,
dcs_ignore,
osc_string,
sos_pm_apc_string,
};
/// Transition action is an action that can be taken during a state
/// transition. This is more of an internal action, not one used by
/// end users, typically.
pub const TransitionAction = enum {
none,
ignore,
print,
execute,
collect,
param,
esc_dispatch,
csi_dispatch,
put,
osc_put,
apc_put,
};
/// Action is the action that a caller of the parser is expected to
/// take as a result of some input character.
pub const Action = union(enum) {
pub const Tag = std.meta.FieldEnum(Action);
/// Draw character to the screen. This is a unicode codepoint.
print: u21,
/// Execute the C0 or C1 function.
execute: u8,
/// Execute the CSI command. Note that pointers within this
/// structure are only valid until the next call to "next".
csi_dispatch: CSI,
/// Execute the ESC command.
esc_dispatch: ESC,
/// Execute the OSC command.
osc_dispatch: osc.Command,
/// DCS-related events.
dcs_hook: DCS,
dcs_put: u8,
dcs_unhook: void,
/// APC data
apc_start: void,
apc_put: u8,
apc_end: void,
pub const CSI = struct {
intermediates: []u8,
params: []u16,
final: u8,
sep: Sep,
/// The separator used for CSI params.
pub const Sep = enum { semicolon, colon };
// Implement formatter for logging
pub fn format(
self: CSI,
comptime layout: []const u8,
opts: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = layout;
_ = opts;
try std.fmt.format(writer, "ESC [ {s} {any} {c}", .{
self.intermediates,
self.params,
self.final,
});
}
};
pub const ESC = struct {
intermediates: []u8,
final: u8,
// Implement formatter for logging
pub fn format(
self: ESC,
comptime layout: []const u8,
opts: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = layout;
_ = opts;
try std.fmt.format(writer, "ESC {s} {c}", .{
self.intermediates,
self.final,
});
}
};
pub const DCS = struct {
intermediates: []const u8 = "",
params: []const u16 = &.{},
final: u8,
};
// Implement formatter for logging. This is mostly copied from the
// std.fmt implementation, but we modify it slightly so that we can
// print out custom formats for some of our primitives.
pub fn format(
self: Action,
comptime layout: []const u8,
opts: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = layout;
const T = Action;
const info = @typeInfo(T).Union;
try writer.writeAll(@typeName(T));
if (info.tag_type) |TagType| {
try writer.writeAll("{ .");
try writer.writeAll(@tagName(@as(TagType, self)));
try writer.writeAll(" = ");
inline for (info.fields) |u_field| {
// If this is the active field...
if (self == @field(TagType, u_field.name)) {
const value = @field(self, u_field.name);
switch (@TypeOf(value)) {
// Unicode
u21 => try std.fmt.format(writer, "'{u}' (U+{X})", .{ value, value }),
// Byte
u8 => try std.fmt.format(writer, "0x{x}", .{value}),
// Note: we don't do ASCII (u8) because there are a lot
// of invisible characters we don't want to handle right
// now.
// All others do the default behavior
else => try std.fmt.formatType(
@field(self, u_field.name),
"any",
opts,
writer,
3,
),
}
}
}
try writer.writeAll(" }");
} else {
try format(writer, "@{x}", .{@intFromPtr(&self)});
}
}
};
/// Keeps track of the parameter sep used for CSI params. We allow colons
/// to be used ONLY by the 'm' CSI action.
pub const ParamSepState = enum(u8) {
none = 0,
semicolon = ';',
colon = ':',
mixed = 1,
};
/// Maximum number of intermediate characters during parsing. This is
/// 4 because we also use the intermediates array for UTF8 decoding which
/// can be at most 4 bytes.
const MAX_INTERMEDIATE = 4;
const MAX_PARAMS = 16;
/// Current state of the state machine
state: State = .ground,
/// Intermediate tracking.
intermediates: [MAX_INTERMEDIATE]u8 = undefined,
intermediates_idx: u8 = 0,
/// Param tracking, building
params: [MAX_PARAMS]u16 = undefined,
params_idx: u8 = 0,
params_sep: ParamSepState = .none,
param_acc: u16 = 0,
param_acc_idx: u8 = 0,
/// Parser for OSC sequences
osc_parser: osc.Parser = .{},
pub fn init() Parser {
return .{};
}
pub fn deinit(self: *Parser) void {
self.osc_parser.deinit();
}
/// Next consumes the next character c and returns the actions to execute.
/// Up to 3 actions may need to be executed -- in order -- representing
/// the state exit, transition, and entry actions.
pub fn next(self: *Parser, c: u8) [3]?Action {
const effect = table[c][@intFromEnum(self.state)];
// log.info("next: {x}", .{c});
const next_state = effect.state;
const action = effect.action;
// After generating the actions, we set our next state.
defer self.state = next_state;
// When going from one state to another, the actions take place in this order:
//
// 1. exit action from old state
// 2. transition action
// 3. entry action to new state
return [3]?Action{
// Exit depends on current state
if (self.state == next_state) null else switch (self.state) {
.osc_string => if (self.osc_parser.end(c)) |cmd|
Action{ .osc_dispatch = cmd }
else
null,
.dcs_passthrough => Action{ .dcs_unhook = {} },
.sos_pm_apc_string => Action{ .apc_end = {} },
else => null,
},
self.doAction(action, c),
// 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 => osc_string: {
self.osc_parser.reset();
break :osc_string null;
},
.dcs_passthrough => Action{
.dcs_hook = .{
.intermediates = self.intermediates[0..self.intermediates_idx],
.params = self.params[0..self.params_idx],
.final = c,
},
},
.sos_pm_apc_string => Action{ .apc_start = {} },
else => null,
},
};
}
pub fn collect(self: *Parser, c: u8) void {
if (self.intermediates_idx >= MAX_INTERMEDIATE) {
log.warn("invalid intermediates count", .{});
return;
}
self.intermediates[self.intermediates_idx] = c;
self.intermediates_idx += 1;
}
fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action {
return switch (action) {
.none, .ignore => null,
.print => Action{ .print = c },
.execute => Action{ .execute = c },
.collect => collect: {
self.collect(c);
break :collect null;
},
.param => param: {
// Semicolon separates parameters. If we encounter a semicolon
// we need to store and move on to the next parameter.
if (c == ';' or c == ':') {
// Ignore too many parameters
if (self.params_idx >= MAX_PARAMS) break :param null;
// If this is our first time seeing a parameter, we track
// the separator used so that we can't mix separators later.
if (self.params_idx == 0) self.params_sep = @enumFromInt(c);
if (@as(ParamSepState, @enumFromInt(c)) != self.params_sep) self.params_sep = .mixed;
// Set param final value
self.params[self.params_idx] = self.param_acc;
self.params_idx += 1;
// Reset current param value to 0
self.param_acc = 0;
self.param_acc_idx = 0;
break :param null;
}
// A numeric value. Add it to our accumulator.
if (self.param_acc_idx > 0) {
self.param_acc *|= 10;
}
self.param_acc +|= c - '0';
// Increment our accumulator index. If we overflow then
// we're out of bounds and we exit immediately.
self.param_acc_idx, const overflow = @addWithOverflow(self.param_acc_idx, 1);
if (overflow > 0) break :param null;
// 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: {
// Ignore too many parameters
if (self.params_idx >= MAX_PARAMS) break :csi_dispatch null;
// Finalize parameters if we have one
if (self.param_acc_idx > 0) {
self.params[self.params_idx] = self.param_acc;
self.params_idx += 1;
}
const result: Action = .{
.csi_dispatch = .{
.intermediates = self.intermediates[0..self.intermediates_idx],
.params = self.params[0..self.params_idx],
.final = c,
.sep = switch (self.params_sep) {
.none, .semicolon => .semicolon,
.colon => .colon,
// There is nothing that treats mixed separators specially
// afaik so we just treat it as a semicolon.
.mixed => .semicolon,
},
},
};
// We only allow colon or mixed separators for the 'm' command.
switch (self.params_sep) {
.none => {},
.semicolon => {},
.colon, .mixed => if (c != 'm') {
log.warn(
"CSI colon or mixed separators only allowed for 'm' command, got: {}",
.{result},
);
break :csi_dispatch null;
},
}
break :csi_dispatch result;
},
.esc_dispatch => Action{
.esc_dispatch = .{
.intermediates = self.intermediates[0..self.intermediates_idx],
.final = c,
},
},
.put => Action{ .dcs_put = c },
.apc_put => Action{ .apc_put = c },
};
}
pub fn clear(self: *Parser) void {
self.intermediates_idx = 0;
self.params_idx = 0;
self.params_sep = .none;
self.param_acc = 0;
self.param_acc_idx = 0;
}
test {
var p = init();
_ = p.next(0x9E);
try testing.expect(p.state == .sos_pm_apc_string);
_ = p.next(0x9C);
try testing.expect(p.state == .ground);
{
const a = p.next('a');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .print);
try testing.expect(a[2] == null);
}
{
const a = p.next(0x19);
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .execute);
try testing.expect(a[2] == null);
}
}
test "esc: ESC ( B" {
var p = init();
_ = p.next(0x1B);
_ = p.next('(');
{
const a = p.next('B');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .esc_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.esc_dispatch;
try testing.expect(d.final == 'B');
try testing.expect(d.intermediates.len == 1);
try testing.expect(d.intermediates[0] == '(');
}
}
test "csi: ESC [ H" {
var p = init();
_ = p.next(0x1B);
_ = p.next(0x5B);
{
const a = p.next(0x48);
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 0x48);
try testing.expect(d.params.len == 0);
}
}
test "csi: ESC [ 1 ; 4 H" {
var p = init();
_ = p.next(0x1B);
_ = p.next(0x5B);
_ = p.next(0x31); // 1
_ = p.next(0x3B); // ;
_ = p.next(0x34); // 4
{
const a = p.next(0x48); // H
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'H');
try testing.expect(d.params.len == 2);
try testing.expectEqual(@as(u16, 1), d.params[0]);
try testing.expectEqual(@as(u16, 4), d.params[1]);
}
}
test "csi: SGR ESC [ 38 : 2 m" {
var p = init();
_ = p.next(0x1B);
_ = p.next('[');
_ = p.next('3');
_ = p.next('8');
_ = p.next(':');
_ = p.next('2');
{
const a = p.next('m');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'm');
try testing.expect(d.sep == .colon);
try testing.expect(d.params.len == 2);
try testing.expectEqual(@as(u16, 38), d.params[0]);
try testing.expectEqual(@as(u16, 2), d.params[1]);
}
}
test "csi: SGR colon followed by semicolon" {
var p = init();
_ = p.next(0x1B);
for ("[48:2") |c| {
const a = p.next(c);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
{
const a = p.next('m');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
}
_ = p.next(0x1B);
_ = p.next('[');
{
const a = p.next('H');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
}
}
test "csi: SGR mixed colon and semicolon" {
var p = init();
_ = p.next(0x1B);
for ("[38:5:1;48:5:0") |c| {
const a = p.next(c);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
{
const a = p.next('m');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
}
}
test "csi: SGR ESC [ 48 : 2 m" {
var p = init();
_ = p.next(0x1B);
for ("[48:2:240:143:104") |c| {
const a = p.next(c);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
{
const a = p.next('m');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'm');
try testing.expect(d.sep == .colon);
try testing.expect(d.params.len == 5);
try testing.expectEqual(@as(u16, 48), d.params[0]);
try testing.expectEqual(@as(u16, 2), d.params[1]);
try testing.expectEqual(@as(u16, 240), d.params[2]);
try testing.expectEqual(@as(u16, 143), d.params[3]);
try testing.expectEqual(@as(u16, 104), d.params[4]);
}
}
test "csi: SGR ESC [4:3m colon" {
var p = init();
_ = p.next(0x1B);
_ = p.next('[');
_ = p.next('4');
_ = p.next(':');
_ = p.next('3');
{
const a = p.next('m');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'm');
try testing.expect(d.sep == .colon);
try testing.expect(d.params.len == 2);
try testing.expectEqual(@as(u16, 4), d.params[0]);
try testing.expectEqual(@as(u16, 3), d.params[1]);
}
}
test "csi: SGR with many blank and colon" {
var p = init();
_ = p.next(0x1B);
for ("[58:2::240:143:104") |c| {
const a = p.next(c);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
{
const a = p.next('m');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'm');
try testing.expect(d.sep == .colon);
try testing.expect(d.params.len == 6);
try testing.expectEqual(@as(u16, 58), d.params[0]);
try testing.expectEqual(@as(u16, 2), d.params[1]);
try testing.expectEqual(@as(u16, 0), d.params[2]);
try testing.expectEqual(@as(u16, 240), d.params[3]);
try testing.expectEqual(@as(u16, 143), d.params[4]);
try testing.expectEqual(@as(u16, 104), d.params[5]);
}
}
test "csi: colon for non-m final" {
var p = init();
_ = p.next(0x1B);
for ("[38:2h") |c| {
const a = p.next(c);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
try testing.expect(p.state == .ground);
}
test "csi: request mode decrqm" {
var p = init();
_ = p.next(0x1B);
for ("[?2026$") |c| {
const a = p.next(c);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
{
const a = p.next('p');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'p');
try testing.expectEqual(@as(usize, 2), d.intermediates.len);
try testing.expectEqual(@as(usize, 1), d.params.len);
try testing.expectEqual(@as(u16, '?'), d.intermediates[0]);
try testing.expectEqual(@as(u16, '$'), d.intermediates[1]);
try testing.expectEqual(@as(u16, 2026), d.params[0]);
}
}
test "csi: change cursor" {
var p = init();
_ = p.next(0x1B);
for ("[3 ") |c| {
const a = p.next(c);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
{
const a = p.next('q');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const d = a[1].?.csi_dispatch;
try testing.expect(d.final == 'q');
try testing.expectEqual(@as(usize, 1), d.intermediates.len);
try testing.expectEqual(@as(usize, 1), d.params.len);
try testing.expectEqual(@as(u16, ' '), d.intermediates[0]);
try testing.expectEqual(@as(u16, 3), d.params[0]);
}
}
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);
try testing.expectEqualStrings("abc", cmd.change_window_title);
}
}
test "osc: change window title (end in esc)" {
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(0x1B);
_ = p.next('\\');
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);
try testing.expectEqualStrings("abc", cmd.change_window_title);
}
}
// https://github.com/darrenstarr/VtNetCore/pull/14
// Saw this on HN, decided to add a test case because why not.
test "osc: 112 incomplete sequence" {
var p = init();
_ = p.next(0x1B);
_ = p.next(']');
_ = p.next('1');
_ = p.next('1');
_ = p.next('2');
{
const a = p.next(0x07);
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 == .reset_color);
try testing.expectEqual(cmd.reset_color.kind, .cursor);
}
}
test "csi: too many params" {
var p = init();
_ = p.next(0x1B);
_ = p.next('[');
for (0..100) |_| {
_ = p.next('1');
_ = p.next(';');
}
_ = p.next('1');
{
const a = p.next('C');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
}

View File

@ -925,7 +925,7 @@ pub fn dumpStringAlloc(
/// This is basically a really jank version of Terminal.printString. We
/// have to reimplement it here because we want a way to print to the screen
/// to test it but don't want all the features of Terminal.
fn testWriteString(self: *Screen, text: []const u8) !void {
pub fn testWriteString(self: *Screen, text: []const u8) !void {
const view = try std.unicode.Utf8View.init(text);
var iter = view.iterator();
while (iter.nextCodepoint()) |c| {

458
src/terminal2/Selection.zig Normal file
View File

@ -0,0 +1,458 @@
//! Represents a single selection within the terminal (i.e. a highlight region).
const Selection = @This();
const std = @import("std");
const assert = std.debug.assert;
const point = @import("point.zig");
const PageList = @import("PageList.zig");
const Screen = @import("Screen.zig");
const Pin = PageList.Pin;
// NOTE(mitchellh): I'm not very happy with how this is implemented, because
// the ordering operations which are used frequently require using
// pointFromPin which -- at the time of writing this -- is slow. The overall
// style of this struct is due to porting it from the previous implementation
// which had an efficient ordering operation.
//
// While reimplementing this, there were too many callers that already
// depended on this behavior so I kept it despite the inefficiency. In the
// future, we should take a look at this again!
/// Start and end of the selection. There is no guarantee that
/// start is before end or vice versa. If a user selects backwards,
/// start will be after end, and vice versa. Use the struct functions
/// to not have to worry about this.
///
/// These are always tracked pins so that they automatically update as
/// the screen they're attached to gets scrolled, erased, etc.
start: *Pin,
end: *Pin,
/// Whether or not this selection refers to a rectangle, rather than whole
/// lines of a buffer. In this mode, start and end refer to the top left and
/// bottom right of the rectangle, or vice versa if the selection is backwards.
rectangle: bool = false,
/// Initialize a new selection with the given start and end pins on
/// the screen. The screen will be used for pin tracking.
pub fn init(
s: *Screen,
start: Pin,
end: Pin,
rect: bool,
) !Selection {
// Track our pins
const tracked_start = try s.pages.trackPin(start);
errdefer s.pages.untrackPin(tracked_start);
const tracked_end = try s.pages.trackPin(end);
errdefer s.pages.untrackPin(tracked_end);
return .{
.start = tracked_start,
.end = tracked_end,
.rectangle = rect,
};
}
pub fn deinit(
self: Selection,
s: *Screen,
) void {
s.pages.untrackPin(self.start);
s.pages.untrackPin(self.end);
}
/// The order of the selection:
///
/// * forward: start(x, y) is before end(x, y) (top-left to bottom-right).
/// * reverse: end(x, y) is before start(x, y) (bottom-right to top-left).
/// * mirrored_[forward|reverse]: special, rectangle selections only (see below).
///
/// For regular selections, the above also holds for top-right to bottom-left
/// (forward) and bottom-left to top-right (reverse). However, for rectangle
/// selections, both of these selections are *mirrored* as orientation
/// operations only flip the x or y axis, not both. Depending on the y axis
/// direction, this is either mirrored_forward or mirrored_reverse.
///
pub const Order = enum { forward, reverse, mirrored_forward, mirrored_reverse };
pub fn order(self: Selection, s: *const Screen) Order {
const start_pt = s.pages.pointFromPin(.screen, self.start.*).?.screen;
const end_pt = s.pages.pointFromPin(.screen, self.end.*).?.screen;
if (self.rectangle) {
// Reverse (also handles single-column)
if (start_pt.y > end_pt.y and start_pt.x >= end_pt.x) return .reverse;
if (start_pt.y >= end_pt.y and start_pt.x > end_pt.x) return .reverse;
// Mirror, bottom-left to top-right
if (start_pt.y > end_pt.y and start_pt.x < end_pt.x) return .mirrored_reverse;
// Mirror, top-right to bottom-left
if (start_pt.y < end_pt.y and start_pt.x > end_pt.x) return .mirrored_forward;
// Forward
return .forward;
}
if (start_pt.y < end_pt.y) return .forward;
if (start_pt.y > end_pt.y) return .reverse;
if (start_pt.x <= end_pt.x) return .forward;
return .reverse;
}
/// Possible adjustments to the selection.
pub const Adjustment = enum {
left,
right,
up,
down,
home,
end,
page_up,
page_down,
};
/// Adjust the selection by some given adjustment. An adjustment allows
/// a selection to be expanded slightly left, right, up, down, etc.
pub fn adjust(
self: *Selection,
s: *const Screen,
adjustment: Adjustment,
) void {
_ = self;
_ = s;
//const screen_end = Screen.RowIndexTag.screen.maxLen(screen) - 1;
// Note that we always adjusts "end" because end always represents
// the last point of the selection by mouse, not necessarilly the
// top/bottom visually. So this results in the right behavior
// whether the user drags up or down.
switch (adjustment) {
// .up => if (result.end.y == 0) {
// result.end.x = 0;
// } else {
// result.end.y -= 1;
// },
//
// .down => if (result.end.y >= screen_end) {
// result.end.y = screen_end;
// result.end.x = screen.cols - 1;
// } else {
// result.end.y += 1;
// },
//
// .left => {
// // Step left, wrapping to the next row up at the start of each new line,
// // until we find a non-empty cell.
// //
// // This iterator emits the start point first, throw it out.
// var iterator = result.end.iterator(screen, .left_up);
// _ = iterator.next();
// while (iterator.next()) |next| {
// if (screen.getCell(
// .screen,
// next.y,
// next.x,
// ).char != 0) {
// result.end = next;
// break;
// }
// }
// },
// .right => {
// // Step right, wrapping to the next row down at the start of each new line,
// // until we find a non-empty cell.
// var iterator = result.end.iterator(screen, .right_down);
// _ = iterator.next();
// while (iterator.next()) |next| {
// if (next.y > screen_end) break;
// if (screen.getCell(
// .screen,
// next.y,
// next.x,
// ).char != 0) {
// if (next.y > screen_end) {
// result.end.y = screen_end;
// } else {
// result.end = next;
// }
// break;
// }
// }
// },
//
// .page_up => if (screen.rows > result.end.y) {
// result.end.y = 0;
// result.end.x = 0;
// } else {
// result.end.y -= screen.rows;
// },
//
// .page_down => if (screen.rows > screen_end - result.end.y) {
// result.end.y = screen_end;
// result.end.x = screen.cols - 1;
// } else {
// result.end.y += screen.rows;
// },
//
// .home => {
// result.end.y = 0;
// result.end.x = 0;
// },
//
// .end => {
// result.end.y = screen_end;
// result.end.x = screen.cols - 1;
//},
else => @panic("TODO"),
}
}
test "Selection: adjust right" {
const testing = std.testing;
var s = try Screen.init(testing.allocator, 5, 10, 0);
defer s.deinit();
try s.testWriteString("A1234\nB5678\nC1234\nD5678");
// // Simple movement right
// {
// var sel = try Selection.init(
// &s,
// s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
// s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
// false,
// );
// defer sel.deinit(&s);
// sel.adjust(&s, .right);
//
// try testing.expectEqual(point.Point{ .screen = .{
// .x = 5,
// .y = 1,
// } }, s.pages.pointFromPin(.screen, sel.start.*).?);
// try testing.expectEqual(point.Point{ .screen = .{
// .x = 4,
// .y = 3,
// } }, s.pages.pointFromPin(.screen, sel.end.*).?);
// }
// // Already at end of the line.
// {
// const sel = (Selection{
// .start = .{ .x = 5, .y = 1 },
// .end = .{ .x = 4, .y = 2 },
// }).adjust(&screen, .right);
//
// try testing.expectEqual(Selection{
// .start = .{ .x = 5, .y = 1 },
// .end = .{ .x = 0, .y = 3 },
// }, sel);
// }
//
// // Already at end of the screen
// {
// const sel = (Selection{
// .start = .{ .x = 5, .y = 1 },
// .end = .{ .x = 4, .y = 3 },
// }).adjust(&screen, .right);
//
// try testing.expectEqual(Selection{
// .start = .{ .x = 5, .y = 1 },
// .end = .{ .x = 4, .y = 3 },
// }, sel);
// }
}
test "Selection: order, standard" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try Screen.init(alloc, 100, 100, 1);
defer s.deinit();
{
// forward, multi-line
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse, multi-line
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
{
// forward, same-line
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// forward, single char
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse, single line
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
false,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
}
test "Selection: order, rectangle" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try Screen.init(alloc, 100, 100, 1);
defer s.deinit();
// Conventions:
// TL - top left
// BL - bottom left
// TR - top right
// BR - bottom right
{
// forward (TL -> BR)
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse (BR -> TL)
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
{
// mirrored_forward (TR -> BL)
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .mirrored_forward);
}
{
// mirrored_reverse (BL -> TR)
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .mirrored_reverse);
}
{
// forward, single line (left -> right )
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse, single line (right -> left)
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
{
// forward, single column (top -> bottom)
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
{
// reverse, single column (bottom -> top)
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .reverse);
}
{
// forward, single cell
const sel = try Selection.init(
&s,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
true,
);
defer sel.deinit(&s);
try testing.expect(sel.order(&s) == .forward);
}
}

View File

@ -0,0 +1,142 @@
//! DFA-based non-allocating error-replacing UTF-8 decoder.
//!
//! This implementation is based largely on the excellent work of
//! Bjoern Hoehrmann, with slight modifications to support error-
//! replacement.
//!
//! For details on Bjoern's DFA-based UTF-8 decoder, see
//! http://bjoern.hoehrmann.de/utf-8/decoder/dfa (MIT licensed)
const UTF8Decoder = @This();
const std = @import("std");
const testing = std.testing;
const log = std.log.scoped(.utf8decoder);
// zig fmt: off
const char_classes = [_]u4{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,
};
const transitions = [_]u8 {
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
12,36,12,12,12,12,12,12,12,12,12,12,
};
// zig fmt: on
// DFA states
const ACCEPT_STATE = 0;
const REJECT_STATE = 12;
// This is where we accumulate our current codepoint.
accumulator: u21 = 0,
// The internal state of the DFA.
state: u8 = ACCEPT_STATE,
/// Takes the next byte in the utf-8 sequence and emits a tuple of
/// - The codepoint that was generated, if there is one.
/// - A boolean that indicates whether the provided byte was consumed.
///
/// The only case where the byte is not consumed is if an ill-formed
/// sequence is reached, in which case a replacement character will be
/// emitted and the byte will not be consumed.
///
/// If the byte is not consumed, the caller is responsible for calling
/// again with the same byte before continuing.
pub inline fn next(self: *UTF8Decoder, byte: u8) struct { ?u21, bool } {
const char_class = char_classes[byte];
const initial_state = self.state;
if (self.state != ACCEPT_STATE) {
self.accumulator <<= 6;
self.accumulator |= (byte & 0x3F);
} else {
self.accumulator = (@as(u21, 0xFF) >> char_class) & (byte);
}
self.state = transitions[self.state + char_class];
if (self.state == ACCEPT_STATE) {
defer self.accumulator = 0;
// Emit the fully decoded codepoint.
return .{ self.accumulator, true };
} else if (self.state == REJECT_STATE) {
self.accumulator = 0;
self.state = ACCEPT_STATE;
// Emit a replacement character. If we rejected the first byte
// in a sequence, then it was consumed, otherwise it was not.
return .{ 0xFFFD, initial_state == ACCEPT_STATE };
} else {
// Emit nothing, we're in the middle of a sequence.
return .{ null, true };
}
}
test "ASCII" {
var d: UTF8Decoder = .{};
var out: [13]u8 = undefined;
for ("Hello, World!", 0..) |byte, i| {
const res = d.next(byte);
try testing.expect(res[1]);
if (res[0]) |codepoint| {
out[i] = @intCast(codepoint);
}
}
try testing.expect(std.mem.eql(u8, &out, "Hello, World!"));
}
test "Well formed utf-8" {
var d: UTF8Decoder = .{};
var out: [4]u21 = undefined;
var i: usize = 0;
// 4 bytes, 3 bytes, 2 bytes, 1 byte
for ("😄✤ÁA") |byte| {
var consumed = false;
while (!consumed) {
const res = d.next(byte);
consumed = res[1];
// There are no errors in this sequence, so
// every byte should be consumed first try.
try testing.expect(consumed == true);
if (res[0]) |codepoint| {
out[i] = codepoint;
i += 1;
}
}
}
try testing.expect(std.mem.eql(u21, &out, &[_]u21{ 0x1F604, 0x2724, 0xC1, 0x41 }));
}
test "Partially invalid utf-8" {
var d: UTF8Decoder = .{};
var out: [5]u21 = undefined;
var i: usize = 0;
// Illegally terminated sequence, valid sequence, illegal surrogate pair.
for ("\xF0\x9F😄\xED\xA0\x80") |byte| {
var consumed = false;
while (!consumed) {
const res = d.next(byte);
consumed = res[1];
if (res[0]) |codepoint| {
out[i] = codepoint;
i += 1;
}
}
}
try testing.expect(std.mem.eql(u21, &out, &[_]u21{ 0xFFFD, 0x1F604, 0xFFFD, 0xFFFD, 0xFFFD }));
}

137
src/terminal2/apc.zig Normal file
View File

@ -0,0 +1,137 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const kitty_gfx = @import("kitty/graphics.zig");
const log = std.log.scoped(.terminal_apc);
/// APC command handler. This should be hooked into a terminal.Stream handler.
/// The start/feed/end functions are meant to be called from the terminal.Stream
/// apcStart, apcPut, and apcEnd functions, respectively.
pub const Handler = struct {
state: State = .{ .inactive = {} },
pub fn deinit(self: *Handler) void {
self.state.deinit();
}
pub fn start(self: *Handler) void {
self.state.deinit();
self.state = .{ .identify = {} };
}
pub fn feed(self: *Handler, alloc: Allocator, byte: u8) void {
switch (self.state) {
.inactive => unreachable,
// We're ignoring this APC command, likely because we don't
// recognize it so there is no need to store the data in memory.
.ignore => return,
// We identify the APC command by the first byte.
.identify => {
switch (byte) {
// Kitty graphics protocol
'G' => self.state = .{ .kitty = kitty_gfx.CommandParser.init(alloc) },
// Unknown
else => self.state = .{ .ignore = {} },
}
},
.kitty => |*p| p.feed(byte) catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
self.state = .{ .ignore = {} };
},
}
}
pub fn end(self: *Handler) ?Command {
defer {
self.state.deinit();
self.state = .{ .inactive = {} };
}
return switch (self.state) {
.inactive => unreachable,
.ignore, .identify => null,
.kitty => |*p| kitty: {
const command = p.complete() catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
break :kitty null;
};
break :kitty .{ .kitty = command };
},
};
}
};
pub const State = union(enum) {
/// We're not in the middle of an APC command yet.
inactive: void,
/// We got an unrecognized APC sequence or the APC sequence we
/// recognized became invalid. We're just dropping bytes.
ignore: void,
/// We're waiting to identify the APC sequence. This is done by
/// inspecting the first byte of the sequence.
identify: void,
/// Kitty graphics protocol
kitty: kitty_gfx.CommandParser,
pub fn deinit(self: *State) void {
switch (self.*) {
.inactive, .ignore, .identify => {},
.kitty => |*v| v.deinit(),
}
}
};
/// Possible APC commands.
pub const Command = union(enum) {
kitty: kitty_gfx.Command,
pub fn deinit(self: *Command, alloc: Allocator) void {
switch (self.*) {
.kitty => |*v| v.deinit(alloc),
}
}
};
test "unknown APC command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
h.start();
for ("Xabcdef1234") |c| h.feed(alloc, c);
try testing.expect(h.end() == null);
}
test "garbage Kitty command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
h.start();
for ("Gabcdef1234") |c| h.feed(alloc, c);
try testing.expect(h.end() == null);
}
test "valid Kitty command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
h.start();
const input = "Gf=24,s=10,v=20,hello=world";
for (input) |c| h.feed(alloc, c);
var cmd = h.end().?;
defer cmd.deinit(alloc);
try testing.expect(cmd == .kitty);
}

309
src/terminal2/dcs.zig Normal file
View File

@ -0,0 +1,309 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const terminal = @import("main.zig");
const DCS = terminal.DCS;
const log = std.log.scoped(.terminal_dcs);
/// DCS command handler. This should be hooked into a terminal.Stream handler.
/// The hook/put/unhook functions are meant to be called from the
/// terminal.stream dcsHook, dcsPut, and dcsUnhook functions, respectively.
pub const Handler = struct {
state: State = .{ .inactive = {} },
/// Maximum bytes any DCS command can take. This is to prevent
/// malicious input from causing us to allocate too much memory.
/// This is arbitrarily set to 1MB today, increase if needed.
max_bytes: usize = 1024 * 1024,
pub fn deinit(self: *Handler) void {
self.discard();
}
pub fn hook(self: *Handler, alloc: Allocator, dcs: DCS) void {
assert(self.state == .inactive);
self.state = if (tryHook(alloc, dcs)) |state_| state: {
if (state_) |state| break :state state else {
log.info("unknown DCS hook: {}", .{dcs});
break :state .{ .ignore = {} };
}
} else |err| state: {
log.info(
"error initializing DCS hook, will ignore hook err={}",
.{err},
);
break :state .{ .ignore = {} };
};
}
fn tryHook(alloc: Allocator, dcs: DCS) !?State {
return switch (dcs.intermediates.len) {
1 => switch (dcs.intermediates[0]) {
'+' => switch (dcs.final) {
// XTGETTCAP
// https://github.com/mitchellh/ghostty/issues/517
'q' => .{
.xtgettcap = try std.ArrayList(u8).initCapacity(
alloc,
128, // Arbitrary choice
),
},
else => null,
},
'$' => switch (dcs.final) {
// DECRQSS
'q' => .{
.decrqss = .{},
},
else => null,
},
else => null,
},
else => null,
};
}
pub fn put(self: *Handler, byte: u8) void {
self.tryPut(byte) catch |err| {
// On error we just discard our state and ignore the rest
log.info("error putting byte into DCS handler err={}", .{err});
self.discard();
self.state = .{ .ignore = {} };
};
}
fn tryPut(self: *Handler, byte: u8) !void {
switch (self.state) {
.inactive,
.ignore,
=> {},
.xtgettcap => |*list| {
if (list.items.len >= self.max_bytes) {
return error.OutOfMemory;
}
try list.append(byte);
},
.decrqss => |*buffer| {
if (buffer.len >= buffer.data.len) {
return error.OutOfMemory;
}
buffer.data[buffer.len] = byte;
buffer.len += 1;
},
}
}
pub fn unhook(self: *Handler) ?Command {
defer self.state = .{ .inactive = {} };
return switch (self.state) {
.inactive,
.ignore,
=> null,
.xtgettcap => |list| .{ .xtgettcap = .{ .data = list } },
.decrqss => |buffer| .{ .decrqss = switch (buffer.len) {
0 => .none,
1 => switch (buffer.data[0]) {
'm' => .sgr,
'r' => .decstbm,
's' => .decslrm,
else => .none,
},
2 => switch (buffer.data[0]) {
' ' => switch (buffer.data[1]) {
'q' => .decscusr,
else => .none,
},
else => .none,
},
else => unreachable,
} },
};
}
fn discard(self: *Handler) void {
switch (self.state) {
.inactive,
.ignore,
=> {},
.xtgettcap => |*list| list.deinit(),
.decrqss => {},
}
self.state = .{ .inactive = {} };
}
};
pub const Command = union(enum) {
/// XTGETTCAP
xtgettcap: XTGETTCAP,
/// DECRQSS
decrqss: DECRQSS,
pub fn deinit(self: Command) void {
switch (self) {
.xtgettcap => |*v| {
v.data.deinit();
},
.decrqss => {},
}
}
pub const XTGETTCAP = struct {
data: std.ArrayList(u8),
i: usize = 0,
/// Returns the next terminfo key being requested and null
/// when there are no more keys. The returned value is NOT hex-decoded
/// because we expect to use a comptime lookup table.
pub fn next(self: *XTGETTCAP) ?[]const u8 {
if (self.i >= self.data.items.len) return null;
var rem = self.data.items[self.i..];
const idx = std.mem.indexOf(u8, rem, ";") orelse rem.len;
// Note that if we're at the end, idx + 1 is len + 1 so we're over
// the end but that's okay because our check above is >= so we'll
// never read.
self.i += idx + 1;
return rem[0..idx];
}
};
/// Supported DECRQSS settings
pub const DECRQSS = enum {
none,
sgr,
decscusr,
decstbm,
decslrm,
};
};
const State = union(enum) {
/// We're not in a DCS state at the moment.
inactive: void,
/// We're hooked, but its an unknown DCS command or one that went
/// invalid due to some bad input, so we're ignoring the rest.
ignore: void,
/// XTGETTCAP
xtgettcap: std.ArrayList(u8),
/// DECRQSS
decrqss: struct {
data: [2]u8 = undefined,
len: u2 = 0,
},
};
test "unknown DCS command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .final = 'A' });
try testing.expect(h.state == .ignore);
try testing.expect(h.unhook() == null);
try testing.expect(h.state == .inactive);
}
test "XTGETTCAP command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
for ("536D756C78") |byte| h.put(byte);
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .xtgettcap);
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expect(cmd.xtgettcap.next() == null);
}
test "XTGETTCAP command multiple keys" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
for ("536D756C78;536D756C78") |byte| h.put(byte);
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .xtgettcap);
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expect(cmd.xtgettcap.next() == null);
}
test "XTGETTCAP command invalid data" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
for ("who;536D756C78") |byte| h.put(byte);
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .xtgettcap);
try testing.expectEqualStrings("who", cmd.xtgettcap.next().?);
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expect(cmd.xtgettcap.next() == null);
}
test "DECRQSS command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('m');
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .decrqss);
try testing.expect(cmd.decrqss == .sgr);
}
test "DECRQSS invalid command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('z');
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .decrqss);
try testing.expect(cmd.decrqss == .none);
h.discard();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('"');
h.put(' ');
h.put('q');
try testing.expect(h.unhook() == null);
}

View File

@ -0,0 +1,67 @@
const std = @import("std");
/// An enum(u16) of the available device status requests.
pub const Request = dsr_enum: {
const EnumField = std.builtin.Type.EnumField;
var fields: [entries.len]EnumField = undefined;
for (entries, 0..) |entry, i| {
fields[i] = .{
.name = entry.name,
.value = @as(Tag.Backing, @bitCast(Tag{
.value = entry.value,
.question = entry.question,
})),
};
}
break :dsr_enum @Type(.{ .Enum = .{
.tag_type = Tag.Backing,
.fields = &fields,
.decls = &.{},
.is_exhaustive = true,
} });
};
/// The tag type for our enum is a u16 but we use a packed struct
/// in order to pack the question bit into the tag. The "u16" size is
/// chosen somewhat arbitrarily to match the largest expected size
/// we see as a multiple of 8 bits.
pub const Tag = packed struct(u16) {
pub const Backing = @typeInfo(@This()).Struct.backing_integer.?;
value: u15,
question: bool = false,
test "order" {
const t: Tag = .{ .value = 1 };
const int: Backing = @bitCast(t);
try std.testing.expectEqual(@as(Backing, 1), int);
}
};
pub fn reqFromInt(v: u16, question: bool) ?Request {
inline for (entries) |entry| {
if (entry.value == v and entry.question == question) {
const tag: Tag = .{ .question = question, .value = entry.value };
const int: Tag.Backing = @bitCast(tag);
return @enumFromInt(int);
}
}
return null;
}
/// A single entry of a possible device status request we support. The
/// "question" field determines if it is valid with or without the "?"
/// prefix.
const Entry = struct {
name: [:0]const u8,
value: comptime_int,
question: bool = false, // "?" request
};
/// The full list of device status request entries.
const entries: []const Entry = &.{
.{ .name = "operating_status", .value = 5 },
.{ .name = "cursor_position", .value = 6 },
.{ .name = "color_scheme", .value = 996, .question = true },
};

View File

@ -52,4 +52,5 @@ test {
_ = @import("hash_map.zig");
_ = @import("size.zig");
_ = @import("style.zig");
_ = @import("Selection.zig");
}

1274
src/terminal2/osc.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,389 @@
//! The primary export of this file is "table", which contains a
//! comptime-generated state transition table for VT emulation.
//!
//! This is based on the vt100.net state machine:
//! https://vt100.net/emu/dec_ansi_parser
//! But has some modifications:
//!
//! * csi_param accepts the colon character (':') since the SGR command
//! accepts colon as a valid parameter value.
//!
const std = @import("std");
const builtin = @import("builtin");
const parser = @import("Parser.zig");
const State = parser.State;
const Action = parser.TransitionAction;
/// The state transition table. The type is [u8][State]Transition but
/// comptime-generated to be exactly-sized.
pub const table = genTable();
/// Table is the type of the state table. This is dynamically (comptime)
/// generated to be exactly sized.
pub const Table = genTableType(false);
/// OptionalTable is private to this file. We use this to accumulate and
/// detect invalid transitions created.
const OptionalTable = genTableType(true);
// Transition is the transition to take within the table
pub const Transition = struct {
state: State,
action: Action,
};
/// Table is the type of the state transition table.
fn genTableType(comptime optional: bool) type {
const max_u8 = std.math.maxInt(u8);
const stateInfo = @typeInfo(State);
const max_state = stateInfo.Enum.fields.len;
const Elem = if (optional) ?Transition else Transition;
return [max_u8 + 1][max_state]Elem;
}
/// Function to generate the full state transition table for VT emulation.
fn genTable() Table {
@setEvalBranchQuota(20000);
// We accumulate using an "optional" table so we can detect duplicates.
var result: OptionalTable = undefined;
for (0..result.len) |i| {
for (0..result[0].len) |j| {
result[i][j] = null;
}
}
// anywhere transitions
const stateInfo = @typeInfo(State);
inline for (stateInfo.Enum.fields) |field| {
const source: State = @enumFromInt(field.value);
// anywhere => ground
single(&result, 0x18, source, .ground, .execute);
single(&result, 0x1A, source, .ground, .execute);
range(&result, 0x80, 0x8F, source, .ground, .execute);
range(&result, 0x91, 0x97, source, .ground, .execute);
single(&result, 0x99, source, .ground, .execute);
single(&result, 0x9A, source, .ground, .execute);
single(&result, 0x9C, source, .ground, .none);
// anywhere => escape
single(&result, 0x1B, source, .escape, .none);
// anywhere => sos_pm_apc_string
single(&result, 0x98, source, .sos_pm_apc_string, .none);
single(&result, 0x9E, source, .sos_pm_apc_string, .none);
single(&result, 0x9F, source, .sos_pm_apc_string, .none);
// anywhere => csi_entry
single(&result, 0x9B, source, .csi_entry, .none);
// anywhere => dcs_entry
single(&result, 0x90, source, .dcs_entry, .none);
// anywhere => osc_string
single(&result, 0x9D, source, .osc_string, .none);
}
// ground
{
// events
single(&result, 0x19, .ground, .ground, .execute);
range(&result, 0, 0x17, .ground, .ground, .execute);
range(&result, 0x1C, 0x1F, .ground, .ground, .execute);
range(&result, 0x20, 0x7F, .ground, .ground, .print);
}
// escape_intermediate
{
const source = State.escape_intermediate;
single(&result, 0x19, source, source, .execute);
range(&result, 0, 0x17, source, source, .execute);
range(&result, 0x1C, 0x1F, source, source, .execute);
range(&result, 0x20, 0x2F, source, source, .collect);
single(&result, 0x7F, source, source, .ignore);
// => ground
range(&result, 0x30, 0x7E, source, .ground, .esc_dispatch);
}
// sos_pm_apc_string
{
const source = State.sos_pm_apc_string;
// events
single(&result, 0x19, source, source, .apc_put);
range(&result, 0, 0x17, source, source, .apc_put);
range(&result, 0x1C, 0x1F, source, source, .apc_put);
range(&result, 0x20, 0x7F, source, source, .apc_put);
}
// escape
{
const source = State.escape;
// events
single(&result, 0x19, source, source, .execute);
range(&result, 0, 0x17, source, source, .execute);
range(&result, 0x1C, 0x1F, source, source, .execute);
single(&result, 0x7F, source, source, .ignore);
// => ground
range(&result, 0x30, 0x4F, source, .ground, .esc_dispatch);
range(&result, 0x51, 0x57, source, .ground, .esc_dispatch);
range(&result, 0x60, 0x7E, source, .ground, .esc_dispatch);
single(&result, 0x59, source, .ground, .esc_dispatch);
single(&result, 0x5A, source, .ground, .esc_dispatch);
single(&result, 0x5C, source, .ground, .esc_dispatch);
// => escape_intermediate
range(&result, 0x20, 0x2F, source, .escape_intermediate, .collect);
// => sos_pm_apc_string
single(&result, 0x58, source, .sos_pm_apc_string, .none);
single(&result, 0x5E, source, .sos_pm_apc_string, .none);
single(&result, 0x5F, source, .sos_pm_apc_string, .none);
// => dcs_entry
single(&result, 0x50, source, .dcs_entry, .none);
// => csi_entry
single(&result, 0x5B, source, .csi_entry, .none);
// => osc_string
single(&result, 0x5D, source, .osc_string, .none);
}
// dcs_entry
{
const source = State.dcs_entry;
// events
single(&result, 0x19, source, source, .ignore);
range(&result, 0, 0x17, source, source, .ignore);
range(&result, 0x1C, 0x1F, source, source, .ignore);
single(&result, 0x7F, source, source, .ignore);
// => dcs_intermediate
range(&result, 0x20, 0x2F, source, .dcs_intermediate, .collect);
// => dcs_ignore
single(&result, 0x3A, source, .dcs_ignore, .none);
// => dcs_param
range(&result, 0x30, 0x39, source, .dcs_param, .param);
single(&result, 0x3B, source, .dcs_param, .param);
range(&result, 0x3C, 0x3F, source, .dcs_param, .collect);
// => dcs_passthrough
range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none);
}
// dcs_intermediate
{
const source = State.dcs_intermediate;
// events
single(&result, 0x19, source, source, .ignore);
range(&result, 0, 0x17, source, source, .ignore);
range(&result, 0x1C, 0x1F, source, source, .ignore);
range(&result, 0x20, 0x2F, source, source, .collect);
single(&result, 0x7F, source, source, .ignore);
// => dcs_ignore
range(&result, 0x30, 0x3F, source, .dcs_ignore, .none);
// => dcs_passthrough
range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none);
}
// dcs_ignore
{
const source = State.dcs_ignore;
// events
single(&result, 0x19, source, source, .ignore);
range(&result, 0, 0x17, source, source, .ignore);
range(&result, 0x1C, 0x1F, source, source, .ignore);
}
// dcs_param
{
const source = State.dcs_param;
// events
single(&result, 0x19, source, source, .ignore);
range(&result, 0, 0x17, source, source, .ignore);
range(&result, 0x1C, 0x1F, source, source, .ignore);
range(&result, 0x30, 0x39, source, source, .param);
single(&result, 0x3B, source, source, .param);
single(&result, 0x7F, source, source, .ignore);
// => dcs_ignore
single(&result, 0x3A, source, .dcs_ignore, .none);
range(&result, 0x3C, 0x3F, source, .dcs_ignore, .none);
// => dcs_intermediate
range(&result, 0x20, 0x2F, source, .dcs_intermediate, .collect);
// => dcs_passthrough
range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none);
}
// dcs_passthrough
{
const source = State.dcs_passthrough;
// events
single(&result, 0x19, source, source, .put);
range(&result, 0, 0x17, source, source, .put);
range(&result, 0x1C, 0x1F, source, source, .put);
range(&result, 0x20, 0x7E, source, source, .put);
single(&result, 0x7F, source, source, .ignore);
}
// csi_param
{
const source = State.csi_param;
// events
single(&result, 0x19, source, source, .execute);
range(&result, 0, 0x17, source, source, .execute);
range(&result, 0x1C, 0x1F, source, source, .execute);
range(&result, 0x30, 0x39, source, source, .param);
single(&result, 0x3A, source, source, .param);
single(&result, 0x3B, source, source, .param);
single(&result, 0x7F, source, source, .ignore);
// => ground
range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch);
// => csi_ignore
range(&result, 0x3C, 0x3F, source, .csi_ignore, .none);
// => csi_intermediate
range(&result, 0x20, 0x2F, source, .csi_intermediate, .collect);
}
// csi_ignore
{
const source = State.csi_ignore;
// events
single(&result, 0x19, source, source, .execute);
range(&result, 0, 0x17, source, source, .execute);
range(&result, 0x1C, 0x1F, source, source, .execute);
range(&result, 0x20, 0x3F, source, source, .ignore);
single(&result, 0x7F, source, source, .ignore);
// => ground
range(&result, 0x40, 0x7E, source, .ground, .none);
}
// csi_intermediate
{
const source = State.csi_intermediate;
// events
single(&result, 0x19, source, source, .execute);
range(&result, 0, 0x17, source, source, .execute);
range(&result, 0x1C, 0x1F, source, source, .execute);
range(&result, 0x20, 0x2F, source, source, .collect);
single(&result, 0x7F, source, source, .ignore);
// => ground
range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch);
// => csi_ignore
range(&result, 0x30, 0x3F, source, .csi_ignore, .none);
}
// csi_entry
{
const source = State.csi_entry;
// events
single(&result, 0x19, source, source, .execute);
range(&result, 0, 0x17, source, source, .execute);
range(&result, 0x1C, 0x1F, source, source, .execute);
single(&result, 0x7F, source, source, .ignore);
// => ground
range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch);
// => csi_ignore
single(&result, 0x3A, source, .csi_ignore, .none);
// => csi_intermediate
range(&result, 0x20, 0x2F, source, .csi_intermediate, .collect);
// => csi_param
range(&result, 0x30, 0x39, source, .csi_param, .param);
single(&result, 0x3B, source, .csi_param, .param);
range(&result, 0x3C, 0x3F, source, .csi_param, .collect);
}
// osc_string
{
const source = State.osc_string;
// events
single(&result, 0x19, 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, 0xFF, source, source, .osc_put);
// XTerm accepts either BEL or ST for terminating OSC
// sequences, and when returning information, uses the same
// terminator used in a query.
single(&result, 0x07, source, .ground, .none);
}
// Create our immutable version
var final: Table = undefined;
for (0..final.len) |i| {
for (0..final[0].len) |j| {
final[i][j] = result[i][j] orelse transition(@enumFromInt(j), .none);
}
}
return final;
}
fn single(t: *OptionalTable, c: u8, s0: State, s1: State, a: Action) void {
const s0_int = @intFromEnum(s0);
// TODO: enable this but it thinks we're in runtime right now
// if (t[c][s0_int]) |existing| {
// @compileLog(c);
// @compileLog(s0);
// @compileLog(s1);
// @compileLog(existing);
// @compileError("transition set multiple times");
// }
t[c][s0_int] = transition(s1, a);
}
fn range(t: *OptionalTable, from: u8, to: u8, s0: State, s1: State, a: Action) void {
var i = from;
while (i <= to) : (i += 1) {
single(t, i, s0, s1, a);
// If 'to' is 0xFF, our next pass will overflow. Return early to prevent
// the loop from executing it's continue expression
if (i == to) break;
}
}
fn transition(state: State, action: Action) Transition {
return .{ .state = state, .action = action };
}
test {
// This forces comptime-evaluation of table, so we're just testing
// that it succeeds in creation.
_ = table;
}

View File

@ -0,0 +1,13 @@
const std = @import("std");
/// Returns true if the data looks safe to paste.
pub fn isSafePaste(data: []const u8) bool {
return std.mem.indexOf(u8, data, "\n") == null;
}
test isSafePaste {
const testing = std.testing;
try testing.expect(isSafePaste("hello"));
try testing.expect(!isSafePaste("hello\n"));
try testing.expect(!isSafePaste("hello\nworld"));
}

2014
src/terminal2/stream.zig Normal file

File diff suppressed because it is too large Load Diff