mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
terminal2: start Selection
This commit is contained in:
@ -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:
|
||||
|
@ -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
794
src/terminal2/Parser.zig
Normal 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);
|
||||
}
|
||||
}
|
@ -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
458
src/terminal2/Selection.zig
Normal 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);
|
||||
}
|
||||
}
|
142
src/terminal2/UTF8Decoder.zig
Normal file
142
src/terminal2/UTF8Decoder.zig
Normal 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
137
src/terminal2/apc.zig
Normal 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
309
src/terminal2/dcs.zig
Normal 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);
|
||||
}
|
67
src/terminal2/device_status.zig
Normal file
67
src/terminal2/device_status.zig
Normal 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 },
|
||||
};
|
@ -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
1274
src/terminal2/osc.zig
Normal file
File diff suppressed because it is too large
Load Diff
389
src/terminal2/parse_table.zig
Normal file
389
src/terminal2/parse_table.zig
Normal 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;
|
||||
}
|
13
src/terminal2/sanitize.zig
Normal file
13
src/terminal2/sanitize.zig
Normal 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
2014
src/terminal2/stream.zig
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user