mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-21 04:58:09 +00:00
Kitty graphics payloads are dispatched in bulk, but finding each slice
boundary still examines every byte with a scalar loop. This leaves large
direct base64 image transmissions parser-bound.
Scan ordinary APC bytes using the vector width recommended for the compile
target. Keep the scalar scan both as the tail and as the full fallback when
the target has no recommended vector width. Test state-machine boundaries
against byte-at-a-time parsing.
A ReleaseFast APC parser benchmark over the same 64 MiB Kitty graphics
corpus, with 10 warmups and 30 measured runs, produced:
mean median
scalar 37.6 ms 32.6 ms
vectorized 22.3 ms 19.0 ms
Hyperfine reports the vectorized version as 1.69 times faster overall, with
the median runtime improving by approximately 42 percent.
3961 lines
140 KiB
Zig
3961 lines
140 KiB
Zig
const streampkg = @This();
|
|
const std = @import("std");
|
|
const build_options = @import("terminal_options");
|
|
const assert = @import("../quirks.zig").inlineAssert;
|
|
const testing = std.testing;
|
|
const Allocator = std.mem.Allocator;
|
|
const simd = @import("../simd/main.zig");
|
|
const lib = @import("lib.zig");
|
|
const Parser = @import("Parser.zig");
|
|
const ansi = @import("ansi.zig");
|
|
const charsets = @import("charsets.zig");
|
|
const device_attributes = @import("device_attributes.zig");
|
|
const device_status = @import("device_status.zig");
|
|
const csi = @import("csi.zig");
|
|
const kitty = @import("kitty.zig");
|
|
const modes = @import("modes.zig");
|
|
const osc = @import("osc.zig");
|
|
const sgr = @import("sgr.zig");
|
|
const UTF8Decoder = @import("UTF8Decoder.zig");
|
|
const MouseShape = @import("mouse.zig").Shape;
|
|
|
|
const log = std.log.scoped(.stream);
|
|
|
|
/// Flip this to true when you want verbose debug output for
|
|
/// debugging terminal stream issues. In addition to louder
|
|
/// output this will also disable the SIMD optimizations in
|
|
/// order to make it easier to see every byte. So if you're
|
|
/// debugging an issue in the SIMD code then you'll need to
|
|
/// do something else.
|
|
const debug = false;
|
|
|
|
/// The possible actions that can be emitted by the Stream
|
|
/// function for handling.
|
|
pub const Action = union(Key) {
|
|
print: Print,
|
|
print_slice: PrintSlice,
|
|
print_repeat: usize,
|
|
bell,
|
|
backspace,
|
|
horizontal_tab: u16,
|
|
horizontal_tab_back: u16,
|
|
linefeed,
|
|
carriage_return,
|
|
enquiry,
|
|
invoke_charset: InvokeCharset,
|
|
cursor_up: CursorMovement,
|
|
cursor_down: CursorMovement,
|
|
cursor_left: CursorMovement,
|
|
cursor_right: CursorMovement,
|
|
cursor_col: CursorMovement,
|
|
cursor_row: CursorMovement,
|
|
cursor_col_relative: CursorMovement,
|
|
cursor_row_relative: CursorMovement,
|
|
cursor_pos: CursorPos,
|
|
cursor_style: ansi.CursorStyle,
|
|
erase_display_below: bool,
|
|
erase_display_above: bool,
|
|
erase_display_complete: bool,
|
|
erase_display_scrollback: bool,
|
|
erase_display_scroll_complete: bool,
|
|
erase_line_right: bool,
|
|
erase_line_left: bool,
|
|
erase_line_complete: bool,
|
|
erase_line_right_unless_pending_wrap: bool,
|
|
delete_chars: usize,
|
|
erase_chars: usize,
|
|
insert_lines: usize,
|
|
insert_blanks: usize,
|
|
delete_lines: usize,
|
|
scroll_up: usize,
|
|
scroll_down: usize,
|
|
tab_clear_current,
|
|
tab_clear_all,
|
|
tab_set,
|
|
tab_reset,
|
|
index,
|
|
next_line,
|
|
reverse_index,
|
|
full_reset,
|
|
set_mode: Mode,
|
|
reset_mode: Mode,
|
|
save_mode: Mode,
|
|
restore_mode: Mode,
|
|
request_mode: Mode,
|
|
request_mode_unknown: RawMode,
|
|
top_and_bottom_margin: Margin,
|
|
left_and_right_margin: Margin,
|
|
left_and_right_margin_ambiguous,
|
|
save_cursor,
|
|
restore_cursor,
|
|
modify_key_format: ansi.ModifyKeyFormat,
|
|
mouse_shift_capture: bool,
|
|
protected_mode_off,
|
|
protected_mode_iso,
|
|
protected_mode_dec,
|
|
size_report: csi.SizeReportStyle,
|
|
title_push: u16,
|
|
title_pop: u16,
|
|
xtversion,
|
|
device_attributes: device_attributes.Req,
|
|
device_status: DeviceStatus,
|
|
kitty_keyboard_query,
|
|
kitty_keyboard_push: KittyKeyboardFlags,
|
|
kitty_keyboard_pop: u16,
|
|
kitty_keyboard_set: KittyKeyboardFlags,
|
|
kitty_keyboard_set_or: KittyKeyboardFlags,
|
|
kitty_keyboard_set_not: KittyKeyboardFlags,
|
|
dcs_hook: Parser.Action.DCS,
|
|
dcs_put: u8,
|
|
dcs_unhook,
|
|
apc_start,
|
|
apc_end,
|
|
apc_put: u8,
|
|
apc_put_slice: ApcPutSlice,
|
|
end_hyperlink,
|
|
active_status_display: ansi.StatusDisplay,
|
|
decaln,
|
|
window_title: WindowTitle,
|
|
report_pwd: ReportPwd,
|
|
show_desktop_notification: ShowDesktopNotification,
|
|
progress_report: osc.Command.ProgressReport,
|
|
start_hyperlink: StartHyperlink,
|
|
clipboard_contents: ClipboardContents,
|
|
mouse_shape: MouseShape,
|
|
configure_charset: ConfigureCharset,
|
|
set_attribute: sgr.Attribute,
|
|
kitty_color_report: kitty.color.OSC,
|
|
color_operation: ColorOperation,
|
|
semantic_prompt: SemanticPrompt,
|
|
|
|
pub const Key = lib.Enum(
|
|
lib.target,
|
|
&.{
|
|
"print",
|
|
"print_slice",
|
|
"print_repeat",
|
|
"bell",
|
|
"backspace",
|
|
"horizontal_tab",
|
|
"horizontal_tab_back",
|
|
"linefeed",
|
|
"carriage_return",
|
|
"enquiry",
|
|
"invoke_charset",
|
|
"cursor_up",
|
|
"cursor_down",
|
|
"cursor_left",
|
|
"cursor_right",
|
|
"cursor_col",
|
|
"cursor_row",
|
|
"cursor_col_relative",
|
|
"cursor_row_relative",
|
|
"cursor_pos",
|
|
"cursor_style",
|
|
"erase_display_below",
|
|
"erase_display_above",
|
|
"erase_display_complete",
|
|
"erase_display_scrollback",
|
|
"erase_display_scroll_complete",
|
|
"erase_line_right",
|
|
"erase_line_left",
|
|
"erase_line_complete",
|
|
"erase_line_right_unless_pending_wrap",
|
|
"delete_chars",
|
|
"erase_chars",
|
|
"insert_lines",
|
|
"insert_blanks",
|
|
"delete_lines",
|
|
"scroll_up",
|
|
"scroll_down",
|
|
"tab_clear_current",
|
|
"tab_clear_all",
|
|
"tab_set",
|
|
"tab_reset",
|
|
"index",
|
|
"next_line",
|
|
"reverse_index",
|
|
"full_reset",
|
|
"set_mode",
|
|
"reset_mode",
|
|
"save_mode",
|
|
"restore_mode",
|
|
"request_mode",
|
|
"request_mode_unknown",
|
|
"top_and_bottom_margin",
|
|
"left_and_right_margin",
|
|
"left_and_right_margin_ambiguous",
|
|
"save_cursor",
|
|
"restore_cursor",
|
|
"modify_key_format",
|
|
"mouse_shift_capture",
|
|
"protected_mode_off",
|
|
"protected_mode_iso",
|
|
"protected_mode_dec",
|
|
"size_report",
|
|
"title_push",
|
|
"title_pop",
|
|
"xtversion",
|
|
"device_attributes",
|
|
"device_status",
|
|
"kitty_keyboard_query",
|
|
"kitty_keyboard_push",
|
|
"kitty_keyboard_pop",
|
|
"kitty_keyboard_set",
|
|
"kitty_keyboard_set_or",
|
|
"kitty_keyboard_set_not",
|
|
"dcs_hook",
|
|
"dcs_put",
|
|
"dcs_unhook",
|
|
"apc_start",
|
|
"apc_end",
|
|
"apc_put",
|
|
"apc_put_slice",
|
|
"end_hyperlink",
|
|
"active_status_display",
|
|
"decaln",
|
|
"window_title",
|
|
"report_pwd",
|
|
"show_desktop_notification",
|
|
"progress_report",
|
|
"start_hyperlink",
|
|
"clipboard_contents",
|
|
"mouse_shape",
|
|
"configure_charset",
|
|
"set_attribute",
|
|
"kitty_color_report",
|
|
"color_operation",
|
|
"semantic_prompt",
|
|
},
|
|
);
|
|
|
|
/// C ABI functions.
|
|
const c_union = lib.TaggedUnion(
|
|
lib.target,
|
|
@This(),
|
|
// TODO: Before shipping an ABI-compatible libghostty, verify this.
|
|
// This was just arbitrarily chosen for now.
|
|
[16]u64,
|
|
);
|
|
pub const Tag = c_union.Tag;
|
|
pub const Value = c_union.Value;
|
|
pub const C = c_union.C;
|
|
pub const CValue = c_union.CValue;
|
|
pub const cval = c_union.cval;
|
|
|
|
/// Field types
|
|
pub const Print = struct {
|
|
cp: u21,
|
|
|
|
pub const C = extern struct {
|
|
cp: u32,
|
|
};
|
|
|
|
pub fn cval(self: Print) Print.C {
|
|
return .{ .cp = @intCast(self.cp) };
|
|
}
|
|
};
|
|
|
|
/// A run of printable codepoints. This is emitted instead of
|
|
/// individual print actions when the stream can decode multiple
|
|
/// printable codepoints at once, so handlers can process them in
|
|
/// batch with per-run rather than per-codepoint overhead (see
|
|
/// Terminal.printSlice). A naive handler can simply loop and
|
|
/// handle each codepoint like a print action.
|
|
///
|
|
/// The slice is only valid for the duration of the handler call.
|
|
pub const PrintSlice = struct {
|
|
cps: []const u32,
|
|
|
|
pub const C = extern struct {
|
|
cps: [*]const u32,
|
|
len: usize,
|
|
};
|
|
|
|
pub fn cval(self: PrintSlice) PrintSlice.C {
|
|
return .{ .cps = self.cps.ptr, .len = self.cps.len };
|
|
}
|
|
};
|
|
|
|
pub const ApcPutSlice = struct {
|
|
bytes: []const u8,
|
|
|
|
pub const C = extern struct {
|
|
bytes: [*]const u8,
|
|
len: usize,
|
|
};
|
|
|
|
pub fn cval(self: ApcPutSlice) ApcPutSlice.C {
|
|
return .{ .bytes = self.bytes.ptr, .len = self.bytes.len };
|
|
}
|
|
};
|
|
|
|
pub const InvokeCharset = lib.Struct(lib.target, struct {
|
|
bank: charsets.ActiveSlot,
|
|
charset: charsets.Slots,
|
|
locking: bool,
|
|
});
|
|
|
|
pub const CursorMovement = extern struct {
|
|
/// The value of the cursor movement. Depending on the tag of this
|
|
/// union this may be an absolute value or it may be a relative
|
|
/// value. For example, `cursor_up` is relative, but `cursor_row`
|
|
/// is absolute.
|
|
value: u16,
|
|
};
|
|
|
|
pub const CursorPos = extern struct {
|
|
row: u16,
|
|
col: u16,
|
|
};
|
|
|
|
pub const DeviceStatus = struct {
|
|
request: device_status.Request,
|
|
|
|
pub const C = u16;
|
|
|
|
pub fn cval(self: DeviceStatus) DeviceStatus.C {
|
|
return @bitCast(self.request);
|
|
}
|
|
};
|
|
|
|
pub const Mode = struct {
|
|
mode: modes.Mode,
|
|
|
|
pub const C = u16;
|
|
|
|
pub fn cval(self: Mode) Mode.C {
|
|
return @bitCast(self.mode);
|
|
}
|
|
};
|
|
|
|
pub const RawMode = extern struct {
|
|
mode: u16,
|
|
ansi: bool,
|
|
};
|
|
|
|
pub const Margin = extern struct {
|
|
top_left: u16,
|
|
bottom_right: u16,
|
|
};
|
|
|
|
pub const KittyKeyboardFlags = struct {
|
|
flags: kitty.KeyFlags,
|
|
|
|
pub const C = u8;
|
|
|
|
pub fn cval(self: KittyKeyboardFlags) KittyKeyboardFlags.C {
|
|
return @intCast(self.flags.int());
|
|
}
|
|
};
|
|
|
|
pub const WindowTitle = struct {
|
|
title: []const u8,
|
|
|
|
pub const C = lib.String;
|
|
|
|
pub fn cval(self: WindowTitle) WindowTitle.C {
|
|
return .init(self.title);
|
|
}
|
|
};
|
|
|
|
pub const ReportPwd = struct {
|
|
url: []const u8,
|
|
|
|
pub const C = lib.String;
|
|
|
|
pub fn cval(self: ReportPwd) ReportPwd.C {
|
|
return .init(self.url);
|
|
}
|
|
};
|
|
|
|
pub const ShowDesktopNotification = struct {
|
|
title: []const u8,
|
|
body: []const u8,
|
|
|
|
pub const C = extern struct {
|
|
title: lib.String,
|
|
body: lib.String,
|
|
};
|
|
|
|
pub fn cval(self: ShowDesktopNotification) ShowDesktopNotification.C {
|
|
return .{
|
|
.title = .init(self.title),
|
|
.body = .init(self.body),
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const StartHyperlink = struct {
|
|
uri: []const u8,
|
|
id: ?[]const u8,
|
|
|
|
pub const C = extern struct {
|
|
uri: lib.String,
|
|
id: lib.String,
|
|
};
|
|
|
|
pub fn cval(self: StartHyperlink) StartHyperlink.C {
|
|
return .{
|
|
.uri = .init(self.uri),
|
|
.id = .init(self.id orelse ""),
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const ClipboardContents = struct {
|
|
kind: u8,
|
|
data: []const u8,
|
|
|
|
pub const C = extern struct {
|
|
kind: u8,
|
|
data: lib.String,
|
|
};
|
|
|
|
pub fn cval(self: ClipboardContents) ClipboardContents.C {
|
|
return .{
|
|
.kind = self.kind,
|
|
.data = .init(self.data),
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const ConfigureCharset = lib.Struct(lib.target, struct {
|
|
slot: charsets.Slots,
|
|
charset: charsets.Charset,
|
|
});
|
|
|
|
pub const ColorOperation = struct {
|
|
op: osc.color.Operation,
|
|
requests: osc.color.List,
|
|
terminator: osc.Terminator,
|
|
|
|
pub const C = void;
|
|
|
|
pub fn cval(_: ColorOperation) ColorOperation.C {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
pub const SemanticPrompt = osc.Command.SemanticPrompt;
|
|
};
|
|
|
|
/// Returns a type that can process a stream of tty control characters.
|
|
/// This will call the `vt` function on type T with the following signature:
|
|
///
|
|
/// fn(comptime action: Action.Key, value: Action.Value(action)) void
|
|
///
|
|
/// The handler type T can choose to react to whatever actions it cares
|
|
/// about in its pursuit of implementing a terminal emulator or other
|
|
/// functionality.
|
|
///
|
|
/// Note that printable text is delivered via `print_slice` actions
|
|
/// (runs of codepoints) whenever the stream can decode multiple
|
|
/// codepoints at once, and via `print` actions otherwise. Handlers
|
|
/// that care about text must handle both.
|
|
///
|
|
/// The Handler type must also have a `deinit` function.
|
|
///
|
|
/// The "comptime" key is on purpose (vs. a standard Zig tagged union)
|
|
/// because it allows the compiler to optimize away unimplemented actions.
|
|
/// e.g. you don't need to pay a conditional branching cost on every single
|
|
/// action because the Zig compiler codegens separate code paths for every
|
|
/// single action at comptime.
|
|
pub fn Stream(comptime H: type) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
pub const Action = streampkg.Action;
|
|
pub const Handler = H;
|
|
|
|
const T = switch (@typeInfo(Handler)) {
|
|
.pointer => |p| p.child,
|
|
else => Handler,
|
|
};
|
|
|
|
handler: Handler,
|
|
parser: Parser,
|
|
utf8decoder: UTF8Decoder,
|
|
|
|
/// Initialize an allocation-free stream. This will preallocate various
|
|
/// sizes as necessary and anything over that will be dropped. If you
|
|
/// want to support more dynamic behavior use initAlloc instead.
|
|
///
|
|
/// As a concrete example of something that requires heap allocation,
|
|
/// consider OSC 52 (clipboard operations) which can be arbitrarily
|
|
/// large.
|
|
///
|
|
/// If you want to limit allocation size, use an allocator with
|
|
/// a size limit with initAlloc.
|
|
///
|
|
/// This takes ownership of the handler and will call deinit
|
|
/// when the stream is deinitialized.
|
|
pub fn init(h: Handler) Self {
|
|
return .{
|
|
.handler = h,
|
|
.parser = .init(),
|
|
.utf8decoder = .{},
|
|
};
|
|
}
|
|
|
|
/// Initialize the stream that supports heap allocation as necessary.
|
|
pub fn initAlloc(alloc: Allocator, h: Handler) Self {
|
|
var self: Self = .init(h);
|
|
self.parser.osc_parser.alloc = alloc;
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(self: *Self) void {
|
|
self.parser.deinit();
|
|
self.handler.deinit();
|
|
}
|
|
|
|
/// Process a string of characters.
|
|
pub inline fn nextSlice(self: *Self, input: []const u8) void {
|
|
// Disable SIMD optimizations if build requests it or if our
|
|
// manual debug mode is on.
|
|
if (comptime debug or !build_options.simd) {
|
|
for (input) |c| self.next(c);
|
|
return;
|
|
}
|
|
|
|
// This is the maximum number of codepoints we can decode
|
|
// at one time for this function call. This is somewhat arbitrary
|
|
// so if someone can demonstrate a better number then we can switch.
|
|
var cp_buf: [4096]u32 = undefined;
|
|
|
|
// Split the input into chunks that fit into cp_buf.
|
|
var i: usize = 0;
|
|
while (true) {
|
|
const len = @min(cp_buf.len, input.len - i);
|
|
self.nextSliceCapped(input[i .. i + len], &cp_buf);
|
|
i += len;
|
|
if (i >= input.len) break;
|
|
}
|
|
}
|
|
|
|
inline fn nextSliceCapped(
|
|
self: *Self,
|
|
input: []const u8,
|
|
cp_buf: []u32,
|
|
) void {
|
|
assert(input.len <= cp_buf.len);
|
|
|
|
var offset: usize = 0;
|
|
|
|
// If the scalar UTF-8 decoder was in the middle of processing
|
|
// a code sequence, we continue until it's not.
|
|
while (self.utf8decoder.state != 0) {
|
|
if (offset >= input.len) return;
|
|
self.nextUtf8(input[offset]);
|
|
offset += 1;
|
|
}
|
|
if (offset >= input.len) return;
|
|
|
|
// If we're not in the ground state then we process until
|
|
// we are. This can happen if the last chunk of input put us
|
|
// in the middle of a control sequence.
|
|
offset += self.consumeUntilGround(input[offset..]);
|
|
if (offset >= input.len) return;
|
|
offset += self.consumeAllEscapes(input[offset..]);
|
|
|
|
// If we're in the ground state then we can use SIMD to process
|
|
// input until we see an ESC (0x1B), since all other characters
|
|
// up to that point are just UTF-8.
|
|
while (self.parser.state == .ground and offset < input.len) {
|
|
const res = simd.vt.utf8DecodeUntilControlSeq(input[offset..], cp_buf);
|
|
const cps = cp_buf[0..res.decoded];
|
|
|
|
// Hand runs of printable codepoints to the handler as
|
|
// print_slice actions so it can process them with
|
|
// per-run rather than per-codepoint overhead.
|
|
var i: usize = 0;
|
|
while (i < cps.len) {
|
|
const cp = cps[i];
|
|
if (cp <= 0xF) {
|
|
@branchHint(.unlikely);
|
|
self.execute(@intCast(cp));
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
var end = i + 1;
|
|
while (end < cps.len and cps[end] > 0xF) end += 1;
|
|
self.handler.vt(.print_slice, .{ .cps = cps[i..end] });
|
|
i = end;
|
|
}
|
|
// Consume the bytes we just processed.
|
|
offset += res.consumed;
|
|
|
|
if (offset >= input.len) return;
|
|
|
|
// If our offset is NOT an escape then we must have a
|
|
// partial UTF-8 sequence. In that case, we pass it off
|
|
// to the scalar parser.
|
|
if (input[offset] != 0x1B) {
|
|
const rem = input[offset..];
|
|
for (rem) |c| self.nextUtf8(c);
|
|
return;
|
|
}
|
|
|
|
// Process control sequences until we run out.
|
|
offset += self.consumeAllEscapes(input[offset..]);
|
|
}
|
|
}
|
|
|
|
/// Parses back-to-back escape sequences until none are left.
|
|
/// Returns the number of bytes consumed from the provided input.
|
|
///
|
|
/// Expects input to start with 0x1B, use consumeUntilGround first
|
|
/// if the stream may be in the middle of an escape sequence.
|
|
inline fn consumeAllEscapes(self: *Self, input: []const u8) usize {
|
|
var offset: usize = 0;
|
|
while (input[offset] == 0x1B) {
|
|
self.parser.state = .escape;
|
|
self.parser.clear();
|
|
offset += 1;
|
|
offset += self.consumeUntilGround(input[offset..]);
|
|
if (offset >= input.len) return input.len;
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
/// Parses escape sequences until the parser reaches the ground state.
|
|
/// Returns the number of bytes consumed from the provided input.
|
|
inline fn consumeUntilGround(self: *Self, input: []const u8) usize {
|
|
var offset: usize = 0;
|
|
while (self.parser.state != .ground) {
|
|
if (offset >= input.len) return input.len;
|
|
|
|
// Fast path for CSI entry: "ESC [" is by far the most
|
|
// common escape sequence prefix, so handle the '[' and
|
|
// the byte that follows it here rather than paying a
|
|
// nextNonUtf8 call for each.
|
|
if (self.parser.state == .escape and input[offset] == '[') {
|
|
self.parser.state = .csi_entry;
|
|
offset += 1;
|
|
continue;
|
|
}
|
|
|
|
if (comptime !@hasDecl(T, "vtRaw")) {
|
|
if (self.parser.state == .csi_entry) {
|
|
if (self.csiEntryByte(input[offset])) {
|
|
offset += 1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Bulk-consume CSI parameter bytes. This can't be
|
|
// used for handlers with a vtRaw hook because it
|
|
// dispatches the CSI directly (see nextNonUtf8).
|
|
if (self.parser.state == .csi_param) {
|
|
offset += self.consumeCsiParams(input[offset..]);
|
|
if (offset >= input.len) return input.len;
|
|
// If we're still in csi_param then the next byte
|
|
// isn't a parameter byte; let nextNonUtf8 below
|
|
// handle it. Otherwise re-check our state.
|
|
if (self.parser.state != .csi_param) continue;
|
|
}
|
|
|
|
// Bulk-consume APC string bytes into a single slice.
|
|
// APC payloads (e.g. Kitty graphics) can be megabytes
|
|
// of base64 data, so per-byte dispatch is far too slow.
|
|
// This can't be used for handlers with a vtRaw hook
|
|
// because it dispatches the slice directly.
|
|
if (self.parser.state == .sos_pm_apc_string) {
|
|
offset += self.consumeApcString(input[offset..]);
|
|
if (offset >= input.len) return input.len;
|
|
// The next byte exits the string state; let
|
|
// nextNonUtf8 below handle it.
|
|
}
|
|
}
|
|
|
|
self.nextNonUtf8(input[offset]);
|
|
offset += 1;
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
/// Fast path for a byte in the csi_entry state, the state right
|
|
/// after "ESC [". Virtually every CSI sequence spends exactly
|
|
/// one byte in this state, on either a digit, a private marker,
|
|
/// or a final byte. Returns true if the byte was fully handled;
|
|
/// false means the caller must process it through the general
|
|
/// state machine.
|
|
///
|
|
/// Must not be used by handlers with a vtRaw hook because the
|
|
/// final byte case dispatches the CSI directly.
|
|
inline fn csiEntryByte(self: *Self, c: u8) bool {
|
|
comptime assert(!@hasDecl(T, "vtRaw"));
|
|
assert(self.parser.state == .csi_entry);
|
|
switch (c) {
|
|
// First parameter digit.
|
|
'0'...'9' => {
|
|
self.parser.state = .csi_param;
|
|
// param_acc is zero (cleared on escape entry)
|
|
// so accumulating is just the digit value.
|
|
self.parser.param_acc = c - '0';
|
|
self.parser.param_acc_idx = 1;
|
|
},
|
|
// An empty first parameter.
|
|
';' => {
|
|
self.parser.state = .csi_param;
|
|
self.parser.params[0] = 0;
|
|
self.parser.params_idx = 1;
|
|
},
|
|
// Private marker (e.g. '?' in "ESC [ ? 2004 h").
|
|
0x3C...0x3F => {
|
|
self.parser.state = .csi_param;
|
|
self.parser.collect(c);
|
|
},
|
|
// A final byte: a parameterless CSI.
|
|
0x40...0x7E => self.csiDispatchFinal(c),
|
|
// Defer to the state machine for anything else
|
|
// (C0 controls, intermediates, colon).
|
|
else => return false,
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Bulk-consume CSI parameter bytes (digits and separators)
|
|
/// and, if reached, the final byte (dispatching the CSI).
|
|
/// Returns the number of bytes consumed. Stops at the first
|
|
/// byte that isn't handled here, leaving the parser in the
|
|
/// csi_param state so the caller can process that byte.
|
|
fn consumeCsiParams(self: *Self, input: []const u8) usize {
|
|
const p = &self.parser;
|
|
assert(p.state == .csi_param);
|
|
|
|
// Accumulate parser state in locals for the hot loop.
|
|
var acc = p.param_acc;
|
|
var acc_idx = p.param_acc_idx;
|
|
var idx = p.params_idx;
|
|
|
|
var offset: usize = 0;
|
|
while (offset < input.len) {
|
|
const c = input[offset];
|
|
switch (c) {
|
|
// A parameter digit.
|
|
'0'...'9' => {
|
|
if (idx < Parser.MAX_PARAMS) {
|
|
acc *|= 10;
|
|
acc +|= c - '0';
|
|
acc_idx |= 1;
|
|
}
|
|
offset += 1;
|
|
},
|
|
|
|
// A parameter separator.
|
|
':', ';' => {
|
|
if (idx < Parser.MAX_PARAMS) {
|
|
p.params[idx] = acc;
|
|
if (c == ':') p.params_sep.set(idx);
|
|
idx += 1;
|
|
acc = 0;
|
|
acc_idx = 0;
|
|
}
|
|
offset += 1;
|
|
},
|
|
|
|
// A final byte: dispatch the CSI.
|
|
0x40...0x7E => {
|
|
p.param_acc = acc;
|
|
p.param_acc_idx = acc_idx;
|
|
p.params_idx = idx;
|
|
self.csiDispatchFinal(c);
|
|
return offset + 1;
|
|
},
|
|
|
|
// Anything else (C0 controls, intermediates, etc.)
|
|
// is handled by the caller.
|
|
else => break,
|
|
}
|
|
}
|
|
|
|
p.param_acc = acc;
|
|
p.param_acc_idx = acc_idx;
|
|
p.params_idx = idx;
|
|
return offset;
|
|
}
|
|
|
|
/// Bulk-consume APC string bytes and dispatch them as a single
|
|
/// apc_put_slice action. Returns the number of bytes consumed.
|
|
/// Stops at the first byte that is not an apc_put byte in the
|
|
/// parse table, leaving it for the caller to process through
|
|
/// the state machine. CAN, SUB, ESC, and most C1 bytes exit
|
|
/// or abort the string state; 0xA0-0xFF are ignored by the
|
|
/// table (not payload), so they can't be bulk-consumed either.
|
|
///
|
|
/// Must not be used by handlers with a vtRaw hook because it
|
|
/// dispatches the slice directly.
|
|
fn consumeApcString(self: *Self, input: []const u8) usize {
|
|
comptime assert(!@hasDecl(T, "vtRaw"));
|
|
assert(self.parser.state == .sos_pm_apc_string);
|
|
|
|
var end: usize = 0;
|
|
if (comptime std.simd.suggestVectorLength(u8)) |vector_len| {
|
|
const ByteVector = @Vector(vector_len, u8);
|
|
while (end + vector_len <= input.len) {
|
|
const bytes: ByteVector = input[end..][0..vector_len].*;
|
|
const invalid = (bytes == @as(ByteVector, @splat(0x18))) |
|
|
(bytes == @as(ByteVector, @splat(0x1A))) |
|
|
(bytes == @as(ByteVector, @splat(0x1B))) |
|
|
(bytes >= @as(ByteVector, @splat(0x80)));
|
|
if (@reduce(.Or, invalid)) break;
|
|
end += vector_len;
|
|
}
|
|
}
|
|
while (end < input.len) {
|
|
switch (input[end]) {
|
|
// Not apc_put bytes: CAN/SUB/ESC and most C1 exit
|
|
// or abort the state; 0xA0-0xFF are ignored by it.
|
|
0x18, 0x1A, 0x1B, 0x80...0xFF => break,
|
|
// Everything else is an apc_put byte.
|
|
else => end += 1,
|
|
}
|
|
}
|
|
|
|
if (end > 0) self.handler.vt(
|
|
.apc_put_slice,
|
|
.{ .bytes = input[0..end] },
|
|
);
|
|
return end;
|
|
}
|
|
|
|
/// Like nextSlice but takes one byte and is necessarily a scalar
|
|
/// operation that can't use SIMD. Prefer nextSlice if you can and
|
|
/// try to get multiple bytes at once.
|
|
pub inline fn next(self: *Self, c: u8) void {
|
|
// The scalar path can be responsible for decoding UTF-8.
|
|
if (self.parser.state == .ground) {
|
|
self.nextUtf8(c);
|
|
return;
|
|
}
|
|
|
|
self.nextNonUtf8(c);
|
|
}
|
|
|
|
/// Process the next byte and print as necessary.
|
|
///
|
|
/// This assumes we're in the UTF-8 decoding state. If we may not
|
|
/// be in the UTF-8 decoding state call nextSlice or next.
|
|
inline fn nextUtf8(self: *Self, c: u8) void {
|
|
assert(self.parser.state == .ground);
|
|
|
|
const res = self.utf8decoder.next(c);
|
|
const consumed = res[1];
|
|
if (res[0]) |codepoint| {
|
|
self.handleCodepoint(codepoint);
|
|
}
|
|
if (!consumed) {
|
|
// We optimize for the scenario where the text being
|
|
// printed in the terminal ISN'T full of ill-formed
|
|
// UTF-8 sequences.
|
|
@branchHint(.unlikely);
|
|
|
|
const retry = self.utf8decoder.next(c);
|
|
// It should be impossible for the decoder
|
|
// to not consume the byte twice in a row.
|
|
assert(retry[1] == true);
|
|
if (retry[0]) |codepoint| {
|
|
self.handleCodepoint(codepoint);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// To be called whenever the utf-8 decoder produces a codepoint.
|
|
///
|
|
/// This function is abstracted this way to handle the case where
|
|
/// the decoder emits a 0x1B after rejecting an ill-formed sequence.
|
|
inline fn handleCodepoint(self: *Self, c: u21) void {
|
|
// We need to increase the eval branch limit because a lot of
|
|
// tests end up running almost completely at comptime due to
|
|
// a chain of inline functions.
|
|
@setEvalBranchQuota(100_000);
|
|
|
|
// C0 control
|
|
if (c <= 0xF) {
|
|
@branchHint(.unlikely);
|
|
self.execute(@intCast(c));
|
|
return;
|
|
}
|
|
// ESC
|
|
if (c == 0x1B) {
|
|
self.parser.state = .escape;
|
|
self.parser.clear();
|
|
return;
|
|
}
|
|
self.print(@intCast(c));
|
|
}
|
|
|
|
/// Process the next character and call any callbacks if necessary.
|
|
///
|
|
/// This assumes that we're not in the UTF-8 decoding state. If
|
|
/// we may be in the UTF-8 decoding state call nextSlice or next.
|
|
fn nextNonUtf8(self: *Self, c: u8) void {
|
|
assert(self.parser.state != .ground);
|
|
|
|
// Fast path for CSI entry.
|
|
if (self.parser.state == .escape and c == '[') {
|
|
self.parser.state = .csi_entry;
|
|
return;
|
|
}
|
|
|
|
// The fast paths below dispatch actions directly rather than
|
|
// going through Parser.next, so they'd bypass a handler's
|
|
// vtRaw hook. Handlers with vtRaw (e.g. the inspector) use
|
|
// the general path for anything that produces an action.
|
|
const has_vt_raw = comptime @hasDecl(T, "vtRaw");
|
|
|
|
// Fast path for CSI params.
|
|
if (self.parser.state == .csi_param) csi_param: {
|
|
// csi_param is the most common parser state
|
|
// other than ground by a fairly wide margin.
|
|
//
|
|
// ref: https://github.com/qwerasd205/asciinema-stats
|
|
@branchHint(.likely);
|
|
switch (c) {
|
|
// A C0 escape (yes, this is valid):
|
|
0x00...0x0F => self.execute(c),
|
|
// We ignore C0 escapes > 0xF since execute
|
|
// doesn't have processing for them anyway:
|
|
0x10...0x17, 0x19, 0x1C...0x1F => {},
|
|
// We don't currently have any handling for
|
|
// 0x18 or 0x1A, but they should still move
|
|
// the parser state to ground.
|
|
0x18, 0x1A => self.parser.state = .ground,
|
|
// A parameter digit:
|
|
'0'...'9' => if (self.parser.params_idx < Parser.MAX_PARAMS) {
|
|
self.parser.param_acc *|= 10;
|
|
self.parser.param_acc +|= c - '0';
|
|
// The parser's CSI param action uses param_acc_idx
|
|
// to decide if there's a final param that needs to
|
|
// be consumed or not, but it doesn't matter really
|
|
// what it is as long as it's not 0.
|
|
self.parser.param_acc_idx |= 1;
|
|
},
|
|
// A parameter separator:
|
|
':', ';' => if (self.parser.params_idx < Parser.MAX_PARAMS) {
|
|
self.parser.params[self.parser.params_idx] = self.parser.param_acc;
|
|
if (c == ':') self.parser.params_sep.set(self.parser.params_idx);
|
|
self.parser.params_idx += 1;
|
|
|
|
self.parser.param_acc = 0;
|
|
self.parser.param_acc_idx = 0;
|
|
},
|
|
// A final byte: dispatch the CSI directly.
|
|
0x40...0x7E => if (comptime !has_vt_raw) {
|
|
self.csiDispatchFinal(c);
|
|
} else break :csi_param,
|
|
// Explicitly ignored:
|
|
0x7F => {},
|
|
// Defer to the state machine to
|
|
// handle any other characters:
|
|
else => break :csi_param,
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Fast path for CSI entry, the state right after "ESC [".
|
|
if (comptime !has_vt_raw) {
|
|
if (self.parser.state == .csi_entry) {
|
|
if (self.csiEntryByte(c)) return;
|
|
}
|
|
}
|
|
|
|
// We explicitly inline this call here for performance reasons.
|
|
//
|
|
// We do this rather than mark Parser.next as inline because doing
|
|
// that causes weird behavior in some tests- I'm not sure if they
|
|
// miscompile or it's just very counter-intuitive comptime stuff,
|
|
// but regardless, this is the easy solution.
|
|
const actions = @call(.always_inline, Parser.next, .{ &self.parser, c });
|
|
|
|
for (actions) |action_opt| {
|
|
const action = action_opt orelse continue;
|
|
if (comptime debug) log.info("action: {f}", .{action});
|
|
|
|
// A handler can expose this to get the raw action before
|
|
// it is further parsed. If this returns `true` then we skip
|
|
// processing ourselves.
|
|
if (@hasDecl(T, "vtRaw")) {
|
|
const skip = self.handler.vtRaw(action) catch |err| err: {
|
|
log.warn("error handling action manually err={} action={f}", .{
|
|
err,
|
|
action,
|
|
});
|
|
// Always skip erroneous actions because we can't
|
|
// be sure...
|
|
break :err true;
|
|
};
|
|
|
|
if (skip) continue;
|
|
}
|
|
|
|
switch (action) {
|
|
.print => |p| self.print(p),
|
|
.execute => |code| self.execute(code),
|
|
.csi_dispatch => |csi_action| self.csiDispatch(csi_action),
|
|
.esc_dispatch => |esc| self.escDispatch(esc),
|
|
.osc_dispatch => |cmd| self.oscDispatch(cmd),
|
|
.dcs_hook => |dcs| self.handler.vt(.dcs_hook, dcs),
|
|
.dcs_put => |code| self.handler.vt(.dcs_put, code),
|
|
.dcs_unhook => self.handler.vt(.dcs_unhook, {}),
|
|
.apc_start => self.handler.vt(.apc_start, {}),
|
|
.apc_put => |code| self.handler.vt(.apc_put, code),
|
|
.apc_end => self.handler.vt(.apc_end, {}),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Finalize and dispatch a CSI directly from parser state for
|
|
/// the fast paths in nextNonUtf8, without going through
|
|
/// Parser.next. This must match the behavior of the parser's
|
|
/// csi_dispatch action.
|
|
fn csiDispatchFinal(self: *Self, c: u8) void {
|
|
const p = &self.parser;
|
|
p.state = .ground;
|
|
|
|
// Ignore sequences with too many parameters, matching the
|
|
// parser's behavior of dropping the dispatch entirely.
|
|
if (p.params_idx >= Parser.MAX_PARAMS) {
|
|
@branchHint(.unlikely);
|
|
return;
|
|
}
|
|
|
|
// Finalize the last parameter if we have one.
|
|
if (p.param_acc_idx > 0) {
|
|
p.params[p.params_idx] = p.param_acc;
|
|
p.params_idx += 1;
|
|
}
|
|
|
|
const action: Parser.Action.CSI = .{
|
|
.intermediates = p.intermediates[0..p.intermediates_idx],
|
|
.params = p.params[0..p.params_idx],
|
|
.params_sep = p.params_sep,
|
|
.final = c,
|
|
};
|
|
|
|
// We only allow colon or mixed separators for the 'm' command.
|
|
if (c != 'm' and p.params_sep.count() > 0) {
|
|
@branchHint(.cold);
|
|
log.warn(
|
|
"CSI colon or mixed separators only allowed for 'm' command, got: {f}",
|
|
.{action},
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (comptime debug) log.info("action: {f}", .{Parser.Action{ .csi_dispatch = action }});
|
|
self.csiDispatch(action);
|
|
}
|
|
|
|
inline fn print(self: *Self, c: u21) void {
|
|
self.handler.vt(.print, .{ .cp = c });
|
|
}
|
|
|
|
inline fn execute(self: *Self, c: u8) void {
|
|
// If the character is > 0x7F, it's a C1 (8-bit) control,
|
|
// which is strictly equivalent to `ESC` plus `c - 0x40`.
|
|
if (c > 0x7F) {
|
|
@branchHint(.unlikely);
|
|
log.info("executing C1 0x{x} as ESC {c}", .{ c, c - 0x40 });
|
|
self.escDispatch(.{
|
|
.intermediates = &.{},
|
|
.final = c - 0x40,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const c0: ansi.C0 = @enumFromInt(c);
|
|
if (comptime debug) log.info("execute: {f}", .{c0});
|
|
switch (c0) {
|
|
// We ignore SOH/STX: https://github.com/microsoft/terminal/issues/10786
|
|
.NUL, .SOH, .STX => {},
|
|
|
|
.ENQ => self.handler.vt(.enquiry, {}),
|
|
.BEL => self.handler.vt(.bell, {}),
|
|
.BS => self.handler.vt(.backspace, {}),
|
|
.HT => self.handler.vt(.horizontal_tab, 1),
|
|
.LF, .VT, .FF => self.handler.vt(.linefeed, {}),
|
|
.CR => self.handler.vt(.carriage_return, {}),
|
|
.SO => self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G1, .locking = false }),
|
|
.SI => self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G0, .locking = false }),
|
|
|
|
else => logUnsupportedOnce("invalid C0 character, ignoring: 0x{x}", .{c}, c),
|
|
}
|
|
}
|
|
|
|
inline fn csiDispatch(self: *Self, input: Parser.Action.CSI) void {
|
|
// The branch hints here are based on real world data
|
|
// which indicates that the most common CSI finals are:
|
|
//
|
|
// 1. m
|
|
// 2. H
|
|
// 3. K
|
|
// 4. A
|
|
// 5. C
|
|
// 6. X
|
|
// 7. l
|
|
// 8. h
|
|
// 9. r
|
|
//
|
|
// Together, these 9 finals make up about 96% of all
|
|
// CSI sequences encountered in real world scenarios.
|
|
//
|
|
// Additionally, within the prongs, unlikely branch
|
|
// hints have been added to branches that deal with
|
|
// invalid sequences/commands, this is in order to
|
|
// optimize for the happy path where we're getting
|
|
// valid data from the program we're running.
|
|
//
|
|
// ref: https://github.com/qwerasd205/asciinema-stats
|
|
|
|
switch (input.final) {
|
|
// CUU - Cursor Up
|
|
'A', 'k' => {
|
|
@branchHint(.likely);
|
|
switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.cursor_up, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid cursor up command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI A with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
}
|
|
},
|
|
|
|
// CUD - Cursor Down
|
|
'B' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.cursor_down, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid cursor down command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI B with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// CUF - Cursor Right
|
|
'C' => {
|
|
@branchHint(.likely);
|
|
switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.cursor_right, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid cursor right command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI C with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
}
|
|
},
|
|
|
|
// CUB - Cursor Left
|
|
'D', 'j' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.cursor_left, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid cursor left command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI D with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// CNL - Cursor Next Line
|
|
'E' => switch (input.intermediates.len) {
|
|
0 => {
|
|
self.handler.vt(.cursor_down, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid cursor up command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
});
|
|
self.handler.vt(.carriage_return, {});
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI E with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// CPL - Cursor Previous Line
|
|
'F' => switch (input.intermediates.len) {
|
|
0 => {
|
|
self.handler.vt(.cursor_up, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid cursor down command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
});
|
|
self.handler.vt(.carriage_return, {});
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI F with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// HPA - Cursor Horizontal Position Absolute
|
|
// TODO: test
|
|
'G', '`' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.cursor_col, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid HPA command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI G with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// CUP - Set Cursor Position.
|
|
// TODO: test
|
|
'H', 'f' => {
|
|
@branchHint(.likely);
|
|
switch (input.intermediates.len) {
|
|
0 => {
|
|
const pos: streampkg.Action.CursorPos = switch (input.params.len) {
|
|
0 => .{ .row = 1, .col = 1 },
|
|
1 => .{ .row = input.params[0], .col = 1 },
|
|
2 => .{ .row = input.params[0], .col = input.params[1] },
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid CUP command: {f}", .{input});
|
|
return;
|
|
},
|
|
};
|
|
self.handler.vt(.cursor_pos, pos);
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI H with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
}
|
|
},
|
|
|
|
// CHT - Cursor Horizontal Tabulation
|
|
'I' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.horizontal_tab, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid horizontal tab command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI I with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// Erase Display
|
|
'J' => {
|
|
const protected_: ?bool = switch (input.intermediates.len) {
|
|
0 => false,
|
|
1 => if (input.intermediates[0] == '?') true else null,
|
|
else => null,
|
|
};
|
|
|
|
const protected = protected_ orelse {
|
|
log.warn("invalid erase display command: {f}", .{input});
|
|
return;
|
|
};
|
|
|
|
const mode_: ?csi.EraseDisplay = switch (input.params.len) {
|
|
0 => .below,
|
|
1 => std.meta.intToEnum(csi.EraseDisplay, input.params[0]) catch null,
|
|
else => null,
|
|
};
|
|
|
|
const mode = mode_ orelse {
|
|
log.warn("invalid erase display command: {f}", .{input});
|
|
return;
|
|
};
|
|
|
|
switch (mode) {
|
|
.below => self.handler.vt(.erase_display_below, protected),
|
|
.above => self.handler.vt(.erase_display_above, protected),
|
|
.complete => self.handler.vt(.erase_display_complete, protected),
|
|
.scrollback => self.handler.vt(.erase_display_scrollback, protected),
|
|
.scroll_complete => self.handler.vt(.erase_display_scroll_complete, protected),
|
|
}
|
|
},
|
|
|
|
// Erase Line
|
|
'K' => {
|
|
@branchHint(.likely);
|
|
const protected_: ?bool = switch (input.intermediates.len) {
|
|
0 => false,
|
|
1 => if (input.intermediates[0] == '?') true else null,
|
|
else => null,
|
|
};
|
|
|
|
const protected = protected_ orelse {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid erase line command: {f}", .{input});
|
|
return;
|
|
};
|
|
|
|
const mode_: ?csi.EraseLine = switch (input.params.len) {
|
|
0 => .right,
|
|
1 => if (input.params[0] < 3) @enumFromInt(input.params[0]) else null,
|
|
else => null,
|
|
};
|
|
|
|
const mode = mode_ orelse {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid erase line command: {f}", .{input});
|
|
return;
|
|
};
|
|
|
|
switch (mode) {
|
|
.right => self.handler.vt(.erase_line_right, protected),
|
|
.left => self.handler.vt(.erase_line_left, protected),
|
|
.complete => self.handler.vt(.erase_line_complete, protected),
|
|
.right_unless_pending_wrap => self.handler.vt(.erase_line_right_unless_pending_wrap, protected),
|
|
_ => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid erase line mode: {}", .{mode});
|
|
},
|
|
}
|
|
},
|
|
|
|
// IL - Insert Lines
|
|
// TODO: test
|
|
'L' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.insert_lines, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid IL command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI L with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// DL - Delete Lines
|
|
// TODO: test
|
|
'M' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.delete_lines, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid DL command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI M with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// Delete Character (DCH)
|
|
'P' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.delete_chars, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid delete characters command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI P with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// Scroll Up (SD)
|
|
|
|
'S' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.scroll_up, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid scroll up command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI S with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// Scroll Down (SD)
|
|
'T' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.scroll_down, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid scroll down command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI T with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// Cursor Tabulation Control
|
|
'W' => switch (input.intermediates.len) {
|
|
0 => {
|
|
if (input.params.len == 0 or
|
|
(input.params.len == 1 and input.params[0] == 0))
|
|
{
|
|
self.handler.vt(.tab_set, {});
|
|
return;
|
|
}
|
|
|
|
switch (input.params.len) {
|
|
0 => unreachable,
|
|
|
|
1 => switch (input.params[0]) {
|
|
0 => unreachable,
|
|
|
|
2 => self.handler.vt(.tab_clear_current, {}),
|
|
|
|
5 => self.handler.vt(.tab_clear_all, {}),
|
|
|
|
else => {},
|
|
},
|
|
|
|
else => {},
|
|
}
|
|
|
|
log.warn("invalid cursor tabulation control: {f}", .{input});
|
|
return;
|
|
},
|
|
|
|
1 => if (input.intermediates[0] == '?' and
|
|
input.params.len == 1 and
|
|
input.params[0] == 5)
|
|
{
|
|
self.handler.vt(.tab_reset, {});
|
|
} else log.warn("invalid cursor tabulation control: {f}", .{input}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI W with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// Erase Characters (ECH)
|
|
'X' => {
|
|
@branchHint(.likely);
|
|
switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.erase_chars, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid erase characters command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI X with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
}
|
|
},
|
|
|
|
// CHT - Cursor Horizontal Tabulation Back
|
|
'Z' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.horizontal_tab_back, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid horizontal tab back command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI Z with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// HPR - Cursor Horizontal Position Relative
|
|
'a' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.cursor_col_relative, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid HPR command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI a with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// Repeat Previous Char (REP)
|
|
'b' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.print_repeat, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid print repeat command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI b with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// c - Device Attributes (DA1)
|
|
'c' => {
|
|
const req: ?device_attributes.Req = switch (input.intermediates.len) {
|
|
0 => .primary,
|
|
1 => switch (input.intermediates[0]) {
|
|
'>' => .secondary,
|
|
'=' => .tertiary,
|
|
else => null,
|
|
},
|
|
else => null,
|
|
};
|
|
|
|
if (req) |r| {
|
|
self.handler.vt(.device_attributes, r);
|
|
} else {
|
|
logUnsupportedOnce(
|
|
"invalid device attributes command: {f}",
|
|
.{input},
|
|
if (input.params.len > 0) input.params[0] else 0,
|
|
);
|
|
return;
|
|
}
|
|
},
|
|
|
|
// VPA - Cursor Vertical Position Absolute
|
|
'd' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.cursor_row, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid VPA command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI d with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// VPR - Cursor Vertical Position Relative
|
|
'e' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.cursor_row_relative, .{
|
|
.value = switch (input.params.len) {
|
|
0 => 1,
|
|
1 => input.params[0],
|
|
else => {
|
|
log.warn("invalid VPR command: {f}", .{input});
|
|
return;
|
|
},
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI e with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// TBC - Tab Clear
|
|
// TODO: test
|
|
'g' => switch (input.intermediates.len) {
|
|
0 => {
|
|
const mode: csi.TabClear = switch (input.params.len) {
|
|
1 => std.meta.intToEnum(csi.TabClear, input.params[0]) catch {
|
|
log.warn("invalid tab clear mode: {}", .{input.params[0]});
|
|
return;
|
|
},
|
|
else => {
|
|
log.warn("invalid tab clear command: {f}", .{input});
|
|
return;
|
|
},
|
|
};
|
|
switch (mode) {
|
|
.current => self.handler.vt(.tab_clear_current, {}),
|
|
.all => self.handler.vt(.tab_clear_all, {}),
|
|
_ => log.warn("unknown tab clear mode: {}", .{mode}),
|
|
}
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI g with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
// SM - Set Mode
|
|
'h' => mode: {
|
|
@branchHint(.likely);
|
|
const ansi_mode = ansi: {
|
|
if (input.intermediates.len == 0) break :ansi true;
|
|
if (input.intermediates.len == 1 and
|
|
input.intermediates[0] == '?') break :ansi false;
|
|
|
|
log.warn("invalid set mode command: {f}", .{input});
|
|
break :mode;
|
|
};
|
|
|
|
for (input.params) |mode_int| {
|
|
if (modes.modeFromInt(mode_int, ansi_mode)) |mode| {
|
|
self.handler.vt(.set_mode, .{ .mode = mode });
|
|
} else {
|
|
logUnsupportedOnce("unimplemented mode: {}", .{mode_int}, mode_int);
|
|
}
|
|
}
|
|
},
|
|
|
|
// RM - Reset Mode
|
|
'l' => mode: {
|
|
@branchHint(.likely);
|
|
const ansi_mode = ansi: {
|
|
if (input.intermediates.len == 0) break :ansi true;
|
|
if (input.intermediates.len == 1 and
|
|
input.intermediates[0] == '?') break :ansi false;
|
|
|
|
log.warn("invalid set mode command: {f}", .{input});
|
|
break :mode;
|
|
};
|
|
|
|
for (input.params) |mode_int| {
|
|
if (modes.modeFromInt(mode_int, ansi_mode)) |mode| {
|
|
self.handler.vt(.reset_mode, .{ .mode = mode });
|
|
} else {
|
|
logUnsupportedOnce("unimplemented mode: {}", .{mode_int}, mode_int);
|
|
}
|
|
}
|
|
},
|
|
|
|
// SGR - Select Graphic Rendition
|
|
'm' => {
|
|
@branchHint(.likely);
|
|
switch (input.intermediates.len) {
|
|
0 => {
|
|
// This is the most common case.
|
|
@branchHint(.likely);
|
|
// log.info("parse SGR params={any}", .{input.params});
|
|
var p: sgr.Parser = .{
|
|
.params = input.params,
|
|
.params_sep = input.params_sep,
|
|
};
|
|
while (p.next()) |attr| {
|
|
// log.info("SGR attribute: {}", .{attr});
|
|
self.handler.vt(.set_attribute, attr);
|
|
}
|
|
},
|
|
|
|
1 => switch (input.intermediates[0]) {
|
|
'>' => blk: {
|
|
if (input.params.len == 0) {
|
|
// Reset
|
|
self.handler.vt(.modify_key_format, .legacy);
|
|
break :blk;
|
|
}
|
|
|
|
var format: ansi.ModifyKeyFormat = switch (input.params[0]) {
|
|
0 => .legacy,
|
|
1 => .cursor_keys,
|
|
2 => .function_keys,
|
|
4 => .other_keys_none,
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid setModifyKeyFormat: {f}", .{input});
|
|
break :blk;
|
|
},
|
|
};
|
|
|
|
if (input.params.len > 2) {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid setModifyKeyFormat: {f}", .{input});
|
|
break :blk;
|
|
}
|
|
|
|
if (input.params.len == 2) {
|
|
switch (format) {
|
|
// We don't support any of the subparams yet for these.
|
|
.legacy => {},
|
|
.cursor_keys => {},
|
|
.function_keys => {},
|
|
|
|
// We only support the numeric form.
|
|
.other_keys_none => switch (input.params[1]) {
|
|
2 => format = .other_keys_numeric,
|
|
else => {},
|
|
},
|
|
.other_keys_numeric_except => {},
|
|
.other_keys_numeric => {},
|
|
}
|
|
}
|
|
|
|
self.handler.vt(.modify_key_format, format);
|
|
},
|
|
|
|
else => logUnsupportedOnce(
|
|
"unknown CSI m with intermediate: {}",
|
|
.{input.intermediates[0]},
|
|
input.intermediates[0],
|
|
),
|
|
},
|
|
|
|
else => {
|
|
// Nothing, but I wanted a place to put this comment:
|
|
// there are others forms of CSI m that have intermediates.
|
|
// `vim --clean` uses `CSI ? 4 m` and I don't know what
|
|
// that means.
|
|
log.warn(
|
|
"ignoring unimplemented CSI m with intermediates: {s}",
|
|
.{input.intermediates},
|
|
);
|
|
},
|
|
}
|
|
},
|
|
|
|
// TODO: test
|
|
'n' => {
|
|
// Handle deviceStatusReport first
|
|
if (input.intermediates.len == 0 or
|
|
input.intermediates[0] == '?')
|
|
{
|
|
if (input.params.len != 1) {
|
|
log.warn("invalid device status report command: {f}", .{input});
|
|
return;
|
|
}
|
|
|
|
const question = question: {
|
|
if (input.intermediates.len == 0) break :question false;
|
|
if (input.intermediates.len == 1 and
|
|
input.intermediates[0] == '?') break :question true;
|
|
|
|
log.warn("invalid set mode command: {f}", .{input});
|
|
return;
|
|
};
|
|
|
|
const req = device_status.reqFromInt(input.params[0], question) orelse {
|
|
log.warn("invalid device status report command: {f}", .{input});
|
|
return;
|
|
};
|
|
|
|
self.handler.vt(.device_status, .{ .request = req });
|
|
return;
|
|
}
|
|
|
|
// Handle other forms of CSI n
|
|
switch (input.intermediates.len) {
|
|
0 => unreachable, // handled above
|
|
|
|
1 => switch (input.intermediates[0]) {
|
|
'>' => {
|
|
// This isn't strictly correct. CSI > n has parameters that
|
|
// control what exactly is being disabled. However, we
|
|
// only support reverting back to modify other keys in
|
|
// numeric except format.
|
|
self.handler.vt(.modify_key_format, .other_keys_numeric_except);
|
|
},
|
|
|
|
else => log.warn(
|
|
"unknown CSI n with intermediate: {}",
|
|
.{input.intermediates[0]},
|
|
),
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI n with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
}
|
|
},
|
|
|
|
// DECRQM - Request Mode
|
|
'p' => switch (input.intermediates.len) {
|
|
2 => decrqm: {
|
|
const ansi_mode = ansi: {
|
|
switch (input.intermediates.len) {
|
|
1 => if (input.intermediates[0] == '$') break :ansi true,
|
|
2 => if (input.intermediates[0] == '?' and
|
|
input.intermediates[1] == '$') break :ansi false,
|
|
else => {},
|
|
}
|
|
|
|
log.warn(
|
|
"ignoring unimplemented CSI p with intermediates: {s}",
|
|
.{input.intermediates},
|
|
);
|
|
break :decrqm;
|
|
};
|
|
|
|
if (input.params.len != 1) {
|
|
log.warn("invalid DECRQM command: {f}", .{input});
|
|
break :decrqm;
|
|
}
|
|
|
|
const mode_raw = input.params[0];
|
|
const mode = modes.modeFromInt(mode_raw, ansi_mode);
|
|
if (mode) |m| {
|
|
self.handler.vt(.request_mode, .{ .mode = m });
|
|
} else {
|
|
self.handler.vt(.request_mode_unknown, .{
|
|
.mode = mode_raw,
|
|
.ansi = ansi_mode,
|
|
});
|
|
}
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI p with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
'q' => switch (input.intermediates.len) {
|
|
1 => switch (input.intermediates[0]) {
|
|
// DECSCUSR - Select Cursor Style
|
|
// TODO: test
|
|
' ' => {
|
|
const style: ansi.CursorStyle = switch (input.params.len) {
|
|
0 => .default,
|
|
1 => switch (input.params[0]) {
|
|
0 => .default,
|
|
1 => .blinking_block,
|
|
2 => .steady_block,
|
|
3 => .blinking_underline,
|
|
4 => .steady_underline,
|
|
5 => .blinking_bar,
|
|
6 => .steady_bar,
|
|
else => {
|
|
log.warn("invalid cursor style value: {}", .{input.params[0]});
|
|
return;
|
|
},
|
|
},
|
|
else => {
|
|
log.warn("invalid set cursor style command: {f}", .{input});
|
|
return;
|
|
},
|
|
};
|
|
self.handler.vt(.cursor_style, style);
|
|
},
|
|
|
|
// DECSCA
|
|
'"' => {
|
|
const mode_: ?ansi.ProtectedMode = switch (input.params.len) {
|
|
else => null,
|
|
0 => .off,
|
|
1 => switch (input.params[0]) {
|
|
0, 2 => .off,
|
|
1 => .dec,
|
|
else => null,
|
|
},
|
|
};
|
|
|
|
const mode = mode_ orelse {
|
|
log.warn("invalid set protected mode command: {f}", .{input});
|
|
return;
|
|
};
|
|
|
|
switch (mode) {
|
|
.off => self.handler.vt(.protected_mode_off, {}),
|
|
.iso => self.handler.vt(.protected_mode_iso, {}),
|
|
.dec => self.handler.vt(.protected_mode_dec, {}),
|
|
}
|
|
},
|
|
|
|
// XTVERSION
|
|
'>' => self.handler.vt(.xtversion, {}),
|
|
else => {
|
|
log.warn(
|
|
"ignoring unimplemented CSI q with intermediates: {s}",
|
|
.{input.intermediates},
|
|
);
|
|
},
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI p with intermediates: {s}",
|
|
.{input.intermediates},
|
|
),
|
|
},
|
|
|
|
'r' => {
|
|
@branchHint(.likely);
|
|
switch (input.intermediates.len) {
|
|
// DECSTBM - Set Top and Bottom Margins
|
|
0 => switch (input.params.len) {
|
|
0 => self.handler.vt(.top_and_bottom_margin, .{ .top_left = 0, .bottom_right = 0 }),
|
|
1 => self.handler.vt(.top_and_bottom_margin, .{ .top_left = input.params[0], .bottom_right = 0 }),
|
|
2 => self.handler.vt(.top_and_bottom_margin, .{ .top_left = input.params[0], .bottom_right = input.params[1] }),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid DECSTBM command: {f}", .{input});
|
|
},
|
|
},
|
|
|
|
1 => switch (input.intermediates[0]) {
|
|
// Restore Mode
|
|
'?' => {
|
|
for (input.params) |mode_int| {
|
|
if (modes.modeFromInt(mode_int, false)) |mode| {
|
|
self.handler.vt(.restore_mode, .{ .mode = mode });
|
|
} else {
|
|
log.warn(
|
|
"unimplemented restore mode: {}",
|
|
.{mode_int},
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
else => log.warn(
|
|
"unknown CSI s with intermediate: {f}",
|
|
.{input},
|
|
),
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI s with intermediates: {f}",
|
|
.{input},
|
|
),
|
|
}
|
|
},
|
|
|
|
's' => switch (input.intermediates.len) {
|
|
// DECSLRM
|
|
0 => switch (input.params.len) {
|
|
// CSI S is ambiguous with zero params so we defer
|
|
// to our handler to do the proper logic. If mode 69
|
|
// is set, then we should invoke DECSLRM, otherwise
|
|
// we should invoke SC.
|
|
0 => self.handler.vt(.left_and_right_margin_ambiguous, {}),
|
|
1 => self.handler.vt(.left_and_right_margin, .{ .top_left = input.params[0], .bottom_right = 0 }),
|
|
2 => self.handler.vt(.left_and_right_margin, .{ .top_left = input.params[0], .bottom_right = input.params[1] }),
|
|
else => log.warn("invalid DECSLRM command: {f}", .{input}),
|
|
},
|
|
|
|
1 => switch (input.intermediates[0]) {
|
|
'?' => {
|
|
for (input.params) |mode_int| {
|
|
if (modes.modeFromInt(mode_int, false)) |mode| {
|
|
self.handler.vt(.save_mode, .{ .mode = mode });
|
|
} else {
|
|
log.warn(
|
|
"unimplemented save mode: {}",
|
|
.{mode_int},
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
// XTSHIFTESCAPE
|
|
'>' => capture: {
|
|
const capture = switch (input.params.len) {
|
|
0 => false,
|
|
1 => switch (input.params[0]) {
|
|
0 => false,
|
|
1 => true,
|
|
else => {
|
|
log.warn("invalid XTSHIFTESCAPE command: {f}", .{input});
|
|
break :capture;
|
|
},
|
|
},
|
|
else => {
|
|
log.warn("invalid XTSHIFTESCAPE command: {f}", .{input});
|
|
break :capture;
|
|
},
|
|
};
|
|
|
|
self.handler.vt(.mouse_shift_capture, capture);
|
|
},
|
|
|
|
else => log.warn(
|
|
"unknown CSI s with intermediate: {f}",
|
|
.{input},
|
|
),
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI s with intermediates: {f}",
|
|
.{input},
|
|
),
|
|
},
|
|
|
|
// XTWINOPS
|
|
't' => switch (input.intermediates.len) {
|
|
0 => {
|
|
if (input.params.len > 0) {
|
|
switch (input.params[0]) {
|
|
14 => if (input.params.len == 1) {
|
|
// report the text area size in pixels
|
|
self.handler.vt(.size_report, .csi_14_t);
|
|
} else log.warn(
|
|
"ignoring CSI 14 t with extra parameters: {f}",
|
|
.{input},
|
|
),
|
|
16 => if (input.params.len == 1) {
|
|
// report cell size in pixels
|
|
self.handler.vt(.size_report, .csi_16_t);
|
|
} else log.warn(
|
|
"ignoring CSI 16 t with extra parameters: {f}",
|
|
.{input},
|
|
),
|
|
18 => if (input.params.len == 1) {
|
|
// report screen size in characters
|
|
self.handler.vt(.size_report, .csi_18_t);
|
|
} else log.warn(
|
|
"ignoring CSI 18 t with extra parameters: {f}",
|
|
.{input},
|
|
),
|
|
21 => if (input.params.len == 1) {
|
|
// report window title
|
|
self.handler.vt(.size_report, .csi_21_t);
|
|
} else log.warn(
|
|
"ignoring CSI 21 t with extra parameters: {f}",
|
|
.{input},
|
|
),
|
|
inline 22, 23 => |number| if ((input.params.len == 2 or
|
|
input.params.len == 3) and
|
|
// we only support window title
|
|
(input.params[1] == 0 or
|
|
input.params[1] == 2))
|
|
{
|
|
// push/pop title
|
|
const index: u16 = if (input.params.len == 3)
|
|
input.params[2]
|
|
else
|
|
0;
|
|
switch (number) {
|
|
22 => self.handler.vt(.title_push, index),
|
|
23 => self.handler.vt(.title_pop, index),
|
|
else => @compileError("unreachable"),
|
|
}
|
|
} else logUnsupportedOnce(
|
|
"ignoring CSI 22/23 t with extra parameters: {f}",
|
|
.{input},
|
|
input.params[0],
|
|
),
|
|
else => logUnsupportedOnce(
|
|
"ignoring CSI t with unimplemented parameter: {f}",
|
|
.{input},
|
|
input.params[0],
|
|
),
|
|
}
|
|
} else log.err(
|
|
"ignoring CSI t with no parameters: {f}",
|
|
.{input},
|
|
);
|
|
},
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI t with intermediates: {f}",
|
|
.{input},
|
|
),
|
|
},
|
|
|
|
'u' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.restore_cursor, {}),
|
|
|
|
// Kitty keyboard protocol
|
|
1 => switch (input.intermediates[0]) {
|
|
'?' => self.handler.vt(.kitty_keyboard_query, {}),
|
|
|
|
'>' => push: {
|
|
const flags: u5 = if (input.params.len == 1)
|
|
std.math.cast(u5, input.params[0]) orelse {
|
|
log.warn("invalid pushKittyKeyboard command: {f}", .{input});
|
|
break :push;
|
|
}
|
|
else
|
|
0;
|
|
|
|
self.handler.vt(.kitty_keyboard_push, .{ .flags = @as(kitty.KeyFlags, @bitCast(flags)) });
|
|
},
|
|
|
|
'<' => {
|
|
const number: u16 = if (input.params.len == 1)
|
|
input.params[0]
|
|
else
|
|
1;
|
|
|
|
self.handler.vt(.kitty_keyboard_pop, number);
|
|
},
|
|
|
|
'=' => set: {
|
|
const flags: u5 = if (input.params.len >= 1)
|
|
std.math.cast(u5, input.params[0]) orelse {
|
|
log.warn("invalid setKittyKeyboard command: {f}", .{input});
|
|
break :set;
|
|
}
|
|
else
|
|
0;
|
|
|
|
const number: u16 = if (input.params.len >= 2)
|
|
input.params[1]
|
|
else
|
|
1;
|
|
|
|
const action_tag: streampkg.Action.Tag = switch (number) {
|
|
1 => .kitty_keyboard_set,
|
|
2 => .kitty_keyboard_set_or,
|
|
3 => .kitty_keyboard_set_not,
|
|
else => {
|
|
log.warn("invalid setKittyKeyboard command: {f}", .{input});
|
|
break :set;
|
|
},
|
|
};
|
|
|
|
const kitty_flags: streampkg.Action.KittyKeyboardFlags = .{ .flags = @as(kitty.KeyFlags, @bitCast(flags)) };
|
|
switch (action_tag) {
|
|
.kitty_keyboard_set => self.handler.vt(.kitty_keyboard_set, kitty_flags),
|
|
.kitty_keyboard_set_or => self.handler.vt(.kitty_keyboard_set_or, kitty_flags),
|
|
.kitty_keyboard_set_not => self.handler.vt(.kitty_keyboard_set_not, kitty_flags),
|
|
else => unreachable,
|
|
}
|
|
},
|
|
|
|
else => log.warn(
|
|
"unknown CSI s with intermediate: {f}",
|
|
.{input},
|
|
),
|
|
},
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI u: {f}",
|
|
.{input},
|
|
),
|
|
},
|
|
|
|
// ICH - Insert Blanks
|
|
'@' => switch (input.intermediates.len) {
|
|
0 => self.handler.vt(.insert_blanks, switch (input.params.len) {
|
|
0 => 1,
|
|
1 => @max(1, input.params[0]),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid ICH command: {f}", .{input});
|
|
return;
|
|
},
|
|
}),
|
|
|
|
else => log.warn(
|
|
"ignoring unimplemented CSI @: {f}",
|
|
.{input},
|
|
),
|
|
},
|
|
|
|
// DECSASD - Select Active Status Display
|
|
'}' => decsasd: {
|
|
// Verify we're getting a DECSASD command
|
|
if (input.intermediates.len != 1 or input.intermediates[0] != '$') {
|
|
log.warn("unimplemented CSI callback: {f}", .{input});
|
|
break :decsasd;
|
|
}
|
|
if (input.params.len != 1) {
|
|
log.warn("unimplemented CSI callback: {f}", .{input});
|
|
break :decsasd;
|
|
}
|
|
|
|
const display: ansi.StatusDisplay = switch (input.params[0]) {
|
|
0 => .main,
|
|
1 => .status_line,
|
|
else => {
|
|
log.warn("unimplemented CSI callback: {f}", .{input});
|
|
break :decsasd;
|
|
},
|
|
};
|
|
|
|
self.handler.vt(.active_status_display, display);
|
|
},
|
|
|
|
else => log.warn("unimplemented CSI action: {f}", .{input}),
|
|
}
|
|
}
|
|
|
|
inline fn oscDispatch(self: *Self, cmd: osc.Command) void {
|
|
// The branch hints here are based on real world data
|
|
// which indicates that the most common OSC commands are:
|
|
//
|
|
// 1. hyperlink_end
|
|
// 2. change_window_title
|
|
// 3. change_window_icon
|
|
// 4. hyperlink_start
|
|
// 5. report_pwd
|
|
// 6. color_operation
|
|
// 7. semantic_prompt
|
|
//
|
|
// Together, these 7 commands make up about 96% of all
|
|
// OSC commands encountered in real world scenarios.
|
|
//
|
|
// Additionally, within the prongs, unlikely branch
|
|
// hints have been added to branches that deal with
|
|
// invalid sequences/commands, this is in order to
|
|
// optimize for the happy path where we're getting
|
|
// valid data from the program we're running.
|
|
//
|
|
// ref: https://github.com/qwerasd205/asciinema-stats
|
|
|
|
switch (cmd) {
|
|
.semantic_prompt => |sp| {
|
|
@branchHint(.likely);
|
|
self.handler.vt(.semantic_prompt, sp);
|
|
},
|
|
|
|
.change_window_title => |title| {
|
|
@branchHint(.likely);
|
|
if (!std.unicode.utf8ValidateSlice(title)) {
|
|
@branchHint(.unlikely);
|
|
log.warn("change title request: invalid utf-8, ignoring request", .{});
|
|
return;
|
|
}
|
|
|
|
self.handler.vt(.window_title, .{ .title = title });
|
|
},
|
|
|
|
.change_window_icon => |icon| {
|
|
@branchHint(.likely);
|
|
logUnsupportedOnce(
|
|
"OSC 1 (change icon) received and ignored icon={s}",
|
|
.{icon},
|
|
0,
|
|
);
|
|
},
|
|
|
|
.clipboard_contents => |clip| {
|
|
self.handler.vt(.clipboard_contents, .{
|
|
.kind = clip.kind,
|
|
.data = clip.data,
|
|
});
|
|
},
|
|
|
|
.report_pwd => |v| {
|
|
@branchHint(.likely);
|
|
self.handler.vt(.report_pwd, .{ .url = v.value });
|
|
},
|
|
|
|
.mouse_shape => |v| {
|
|
const shape = MouseShape.fromString(v.value) orelse {
|
|
@branchHint(.unlikely);
|
|
log.warn("unknown cursor shape: {s}", .{v.value});
|
|
return;
|
|
};
|
|
|
|
self.handler.vt(.mouse_shape, shape);
|
|
},
|
|
|
|
.color_operation => |v| {
|
|
@branchHint(.likely);
|
|
self.handler.vt(.color_operation, .{
|
|
.op = v.op,
|
|
.requests = v.requests,
|
|
.terminator = v.terminator,
|
|
});
|
|
},
|
|
|
|
.kitty_color_protocol => |v| {
|
|
self.handler.vt(.kitty_color_report, v);
|
|
},
|
|
|
|
.show_desktop_notification => |v| {
|
|
self.handler.vt(.show_desktop_notification, .{
|
|
.title = v.title,
|
|
.body = v.body,
|
|
});
|
|
},
|
|
|
|
.hyperlink_start => |v| {
|
|
@branchHint(.likely);
|
|
self.handler.vt(.start_hyperlink, .{
|
|
.uri = v.uri,
|
|
.id = v.id,
|
|
});
|
|
},
|
|
|
|
.hyperlink_end => {
|
|
@branchHint(.likely);
|
|
self.handler.vt(.end_hyperlink, {});
|
|
},
|
|
|
|
.conemu_progress_report => |v| {
|
|
self.handler.vt(.progress_report, v);
|
|
},
|
|
|
|
.conemu_sleep,
|
|
.conemu_show_message_box,
|
|
.conemu_change_tab_title,
|
|
.conemu_wait_input,
|
|
.conemu_guimacro,
|
|
.conemu_comment,
|
|
.conemu_xterm_emulation,
|
|
.conemu_output_environment_variable,
|
|
.conemu_run_process,
|
|
.kitty_text_sizing,
|
|
.kitty_clipboard_protocol,
|
|
.kitty_dnd_protocol,
|
|
.context_signal,
|
|
=> {
|
|
log.debug("unimplemented OSC callback: {}", .{cmd});
|
|
},
|
|
|
|
.invalid => {
|
|
@branchHint(.cold);
|
|
// This is an invalid internal state, not an invalid OSC
|
|
// string being parsed. We shouldn't see this.
|
|
log.warn("invalid OSC, should never happen", .{});
|
|
},
|
|
}
|
|
}
|
|
|
|
inline fn configureCharset(
|
|
self: *Self,
|
|
intermediates: []const u8,
|
|
set: charsets.Charset,
|
|
) void {
|
|
if (intermediates.len != 1) {
|
|
log.warn("invalid charset intermediate: {any}", .{intermediates});
|
|
return;
|
|
}
|
|
|
|
const slot: charsets.Slots = switch (intermediates[0]) {
|
|
// TODO: support slots '-', '.', '/'
|
|
|
|
'(' => .G0,
|
|
')' => .G1,
|
|
'*' => .G2,
|
|
'+' => .G3,
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid charset intermediate: {any}", .{intermediates});
|
|
return;
|
|
},
|
|
};
|
|
|
|
self.handler.vt(.configure_charset, .{
|
|
.slot = slot,
|
|
.charset = set,
|
|
});
|
|
}
|
|
|
|
inline fn escDispatch(
|
|
self: *Self,
|
|
action: Parser.Action.ESC,
|
|
) void {
|
|
// The branch hints here are based on real world data
|
|
// which indicates that the most common ESC finals are:
|
|
//
|
|
// 1. B
|
|
// 2. \
|
|
// 3. 0
|
|
// 4. M
|
|
// 5. 8
|
|
// 6. 7
|
|
// 7. >
|
|
// 8. =
|
|
//
|
|
// Together, these 8 finals make up nearly 99% of all
|
|
// ESC sequences encountered in real world scenarios.
|
|
//
|
|
// Additionally, within the prongs, unlikely branch
|
|
// hints have been added to branches that deal with
|
|
// invalid sequences/commands, this is in order to
|
|
// optimize for the happy path where we're getting
|
|
// valid data from the program we're running.
|
|
//
|
|
// ref: https://github.com/qwerasd205/asciinema-stats
|
|
|
|
switch (action.final) {
|
|
// Charsets
|
|
'B' => {
|
|
@branchHint(.likely);
|
|
self.configureCharset(action.intermediates, .ascii);
|
|
},
|
|
'A' => self.configureCharset(action.intermediates, .british),
|
|
'0' => {
|
|
@branchHint(.likely);
|
|
self.configureCharset(action.intermediates, .dec_special);
|
|
},
|
|
|
|
// DECSC - Save Cursor
|
|
'7' => {
|
|
@branchHint(.likely);
|
|
switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.save_cursor, {}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid command: {f}", .{action});
|
|
return;
|
|
},
|
|
}
|
|
},
|
|
|
|
'8' => blk: {
|
|
@branchHint(.likely);
|
|
switch (action.intermediates.len) {
|
|
// DECRC - Restore Cursor
|
|
0 => {
|
|
self.handler.vt(.restore_cursor, {});
|
|
break :blk {};
|
|
},
|
|
|
|
1 => switch (action.intermediates[0]) {
|
|
// DECALN - Fill Screen with E
|
|
'#' => {
|
|
self.handler.vt(.decaln, {});
|
|
break :blk {};
|
|
},
|
|
|
|
else => {},
|
|
},
|
|
|
|
else => {}, // fall through
|
|
}
|
|
|
|
logUnsupportedOnce(
|
|
"unimplemented ESC action: {f}",
|
|
.{action},
|
|
action.final,
|
|
);
|
|
},
|
|
|
|
// IND - Index
|
|
'D' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.index, {}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid index command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// NEL - Next Line
|
|
'E' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.next_line, {}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid next line command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// HTS - Horizontal Tab Set
|
|
'H' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.tab_set, {}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid tab set command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// RI - Reverse Index
|
|
'M' => {
|
|
@branchHint(.likely);
|
|
switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.reverse_index, {}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid reverse index command: {f}", .{action});
|
|
return;
|
|
},
|
|
}
|
|
},
|
|
|
|
// SS2 - Single Shift 2
|
|
'N' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.invoke_charset, .{
|
|
.bank = .GL,
|
|
.charset = .G2,
|
|
.locking = true,
|
|
}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid single shift 2 command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// SS3 - Single Shift 3
|
|
'O' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.invoke_charset, .{
|
|
.bank = .GL,
|
|
.charset = .G3,
|
|
.locking = true,
|
|
}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid single shift 3 command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// SPA - Start of Guarded Area
|
|
'V' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.protected_mode_iso, {}),
|
|
else => log.warn("unimplemented ESC callback: {f}", .{action}),
|
|
},
|
|
|
|
// EPA - End of Guarded Area
|
|
'W' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.protected_mode_off, {}),
|
|
else => log.warn("unimplemented ESC callback: {f}", .{action}),
|
|
},
|
|
|
|
// DECID
|
|
'Z' => if (action.intermediates.len == 0) {
|
|
self.handler.vt(.device_attributes, .primary);
|
|
} else log.warn("unimplemented ESC callback: {f}", .{action}),
|
|
|
|
// RIS - Full Reset
|
|
'c' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.full_reset, {}),
|
|
else => {
|
|
log.warn("invalid full reset command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// LS2 - Locking Shift 2
|
|
'n' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.invoke_charset, .{
|
|
.bank = .GL,
|
|
.charset = .G2,
|
|
.locking = false,
|
|
}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid single shift 2 command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// LS3 - Locking Shift 3
|
|
'o' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.invoke_charset, .{
|
|
.bank = .GL,
|
|
.charset = .G3,
|
|
.locking = false,
|
|
}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid single shift 3 command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// LS1R - Locking Shift 1 Right
|
|
'~' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.invoke_charset, .{
|
|
.bank = .GR,
|
|
.charset = .G1,
|
|
.locking = false,
|
|
}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid locking shift 1 right command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// LS2R - Locking Shift 2 Right
|
|
'}' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.invoke_charset, .{
|
|
.bank = .GR,
|
|
.charset = .G2,
|
|
.locking = false,
|
|
}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid locking shift 2 right command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// LS3R - Locking Shift 3 Right
|
|
'|' => switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.invoke_charset, .{
|
|
.bank = .GR,
|
|
.charset = .G3,
|
|
.locking = false,
|
|
}),
|
|
else => {
|
|
@branchHint(.unlikely);
|
|
log.warn("invalid locking shift 3 right command: {f}", .{action});
|
|
return;
|
|
},
|
|
},
|
|
|
|
// Set application keypad mode
|
|
'=' => {
|
|
@branchHint(.likely);
|
|
switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.set_mode, .{ .mode = .keypad_keys }),
|
|
else => log.warn("unimplemented setMode: {f}", .{action}),
|
|
}
|
|
},
|
|
|
|
// Reset application keypad mode
|
|
'>' => {
|
|
@branchHint(.likely);
|
|
switch (action.intermediates.len) {
|
|
0 => self.handler.vt(.reset_mode, .{ .mode = .keypad_keys }),
|
|
else => log.warn("unimplemented setMode: {f}", .{action}),
|
|
}
|
|
},
|
|
|
|
// Sets ST (string terminator). We don't have to do anything
|
|
// because our parser always accepts ST.
|
|
'\\' => {
|
|
@branchHint(.likely);
|
|
},
|
|
|
|
else => logUnsupportedOnce(
|
|
"unimplemented ESC action: {f}",
|
|
.{action},
|
|
action.final,
|
|
),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Logs an unsupported-input message at most once per distinct key
|
|
/// per process.
|
|
///
|
|
/// These messages are emitted in response to input that the terminal
|
|
/// application controls, so a misbehaving (or merely chatty) program
|
|
/// can trigger the same message millions of times, e.g. by toggling
|
|
/// an unimplemented mode on every frame. Each log call has a real
|
|
/// throughput cost (formatting plus a blocking write per message)
|
|
/// while adding no diagnostic value beyond the first occurrence.
|
|
///
|
|
/// The keys seen so far are tracked in a small fixed table (64 bytes)
|
|
/// instantiated per (format, argument type) tuple, i.e. roughly per
|
|
/// call site. Real streams only ever produce a handful of distinct
|
|
/// unsupported values per site, so if the table ever fills, messages
|
|
/// for further new values are suppressed as well: by that point the
|
|
/// log already shows this class of problem and unbounded distinct
|
|
/// values would flood it anyway.
|
|
fn logUnsupportedOnce(
|
|
comptime format: []const u8,
|
|
args: anytype,
|
|
key: u16,
|
|
) void {
|
|
// u32 slots so every u16 key is representable alongside an empty
|
|
// sentinel and so 32-bit targets (e.g. wasm32) have native
|
|
// atomics.
|
|
const empty = std.math.maxInt(u32);
|
|
const Static = struct {
|
|
var seen: [16]u32 = @splat(empty);
|
|
};
|
|
|
|
// The atomics make concurrent streams safe: slots are only ever
|
|
// claimed, never changed, so the scan can stop at the first empty
|
|
// slot. The worst case race is a benign duplicate message.
|
|
for (&Static.seen) |*slot| {
|
|
const cur = @atomicLoad(u32, slot, .acquire);
|
|
if (cur == key) return; // already logged
|
|
if (cur != empty) continue; // other key, keep scanning
|
|
|
|
// Empty slot: claim it for this key and log below.
|
|
const actual = @cmpxchgStrong(
|
|
u32,
|
|
slot,
|
|
empty,
|
|
key,
|
|
.acq_rel,
|
|
.acquire,
|
|
) orelse break;
|
|
|
|
// Lost the race: suppress if it was to the same key, keep
|
|
// scanning otherwise.
|
|
if (actual == key) return;
|
|
} else return; // table full: suppress new values too
|
|
|
|
log.warn(format, args);
|
|
}
|
|
|
|
test Action {
|
|
// Forces the C type to be reified when the target is C, ensuring
|
|
// all our types are C ABI compatible.
|
|
_ = Action.C;
|
|
}
|
|
|
|
test "stream: print" {
|
|
const H = struct {
|
|
c: ?u21 = 0,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.print => self.c = value.cp,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.next('x');
|
|
try testing.expectEqual(@as(u21, 'x'), s.handler.c.?);
|
|
}
|
|
|
|
test "simd: print invalid utf-8" {
|
|
const H = struct {
|
|
c: ?u21 = 0,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.print => self.c = value.cp,
|
|
.print_slice => self.c = @intCast(value.cps[value.cps.len - 1]),
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice(&.{0xFF});
|
|
try testing.expectEqual(@as(u21, 0xFFFD), s.handler.c.?);
|
|
}
|
|
|
|
test "simd: complete incomplete utf-8" {
|
|
const H = struct {
|
|
c: ?u21 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.print => self.c = value.cp,
|
|
.print_slice => self.c = @intCast(value.cps[value.cps.len - 1]),
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice(&.{0xE0}); // 3 byte
|
|
try testing.expect(s.handler.c == null);
|
|
s.nextSlice(&.{0xA0}); // still incomplete
|
|
try testing.expect(s.handler.c == null);
|
|
s.nextSlice(&.{0x80});
|
|
try testing.expectEqual(@as(u21, 0x800), s.handler.c.?);
|
|
}
|
|
|
|
test "stream: cursor right (CUF)" {
|
|
const H = struct {
|
|
amount: u16 = 0,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.cursor_right => self.amount = value.value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[C");
|
|
try testing.expectEqual(@as(u16, 1), s.handler.amount);
|
|
|
|
s.nextSlice("\x1B[5C");
|
|
try testing.expectEqual(@as(u16, 5), s.handler.amount);
|
|
|
|
s.handler.amount = 0;
|
|
s.nextSlice("\x1B[5;4C");
|
|
try testing.expectEqual(@as(u16, 0), s.handler.amount);
|
|
|
|
s.handler.amount = 0;
|
|
s.nextSlice("\x1b[?3C");
|
|
try testing.expectEqual(@as(u16, 0), s.handler.amount);
|
|
}
|
|
|
|
test "stream: dec set mode (SM) and reset mode (RM)" {
|
|
const H = struct {
|
|
mode: modes.Mode = @as(modes.Mode, @enumFromInt(1)),
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.set_mode => self.mode = value.mode,
|
|
.reset_mode => self.mode = @as(modes.Mode, @enumFromInt(1)),
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[?6h");
|
|
try testing.expectEqual(@as(modes.Mode, .origin), s.handler.mode);
|
|
|
|
s.nextSlice("\x1B[?6l");
|
|
try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode);
|
|
|
|
s.handler.mode = @as(modes.Mode, @enumFromInt(1));
|
|
s.nextSlice("\x1B[6 h");
|
|
try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode);
|
|
}
|
|
|
|
test "stream: ansi set mode (SM) and reset mode (RM)" {
|
|
const H = struct {
|
|
mode: ?modes.Mode = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.set_mode => self.mode = value.mode,
|
|
.reset_mode => self.mode = null,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[4h");
|
|
try testing.expectEqual(@as(modes.Mode, .insert), s.handler.mode.?);
|
|
|
|
s.nextSlice("\x1B[4l");
|
|
try testing.expect(s.handler.mode == null);
|
|
|
|
s.handler.mode = null;
|
|
s.nextSlice("\x1B[>5h");
|
|
try testing.expect(s.handler.mode == null);
|
|
}
|
|
|
|
test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" {
|
|
const H = struct {
|
|
mode: ?modes.Mode = null,
|
|
|
|
pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void {
|
|
self.mode = null;
|
|
if (v) self.mode = mode;
|
|
}
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
_ = self;
|
|
_ = value;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[6h");
|
|
try testing.expect(s.handler.mode == null);
|
|
|
|
s.nextSlice("\x1B[6l");
|
|
try testing.expect(s.handler.mode == null);
|
|
}
|
|
|
|
test "stream: restore mode" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
called: bool = false,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: Stream(Self).Action.Tag,
|
|
value: Stream(Self).Action.Value(action),
|
|
) void {
|
|
_ = value;
|
|
switch (action) {
|
|
.top_and_bottom_margin => self.called = true,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
for ("\x1B[?42r") |c| s.next(c);
|
|
try testing.expect(!s.handler.called);
|
|
}
|
|
|
|
test "stream: pop kitty keyboard with no params defaults to 1" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
n: u16 = 0,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.kitty_keyboard_pop => self.n = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
for ("\x1B[<u") |c| s.next(c);
|
|
try testing.expectEqual(@as(u16, 1), s.handler.n);
|
|
}
|
|
|
|
test "stream: DECSCA" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
v: ?ansi.ProtectedMode = null,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: Stream(Self).Action.Tag,
|
|
value: Stream(Self).Action.Value(action),
|
|
) void {
|
|
_ = value;
|
|
switch (action) {
|
|
.protected_mode_off => self.v = .off,
|
|
.protected_mode_iso => self.v = .iso,
|
|
.protected_mode_dec => self.v = .dec,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
{
|
|
for ("\x1B[\"q") |c| s.next(c);
|
|
try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?);
|
|
}
|
|
{
|
|
for ("\x1B[0\"q") |c| s.next(c);
|
|
try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?);
|
|
}
|
|
{
|
|
for ("\x1B[2\"q") |c| s.next(c);
|
|
try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?);
|
|
}
|
|
{
|
|
for ("\x1B[1\"q") |c| s.next(c);
|
|
try testing.expectEqual(ansi.ProtectedMode.dec, s.handler.v.?);
|
|
}
|
|
}
|
|
|
|
test "stream: DECED, DECSED" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
mode: ?csi.EraseDisplay = null,
|
|
protected: ?bool = null,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
switch (action) {
|
|
.erase_display_below => {
|
|
self.mode = .below;
|
|
self.protected = value;
|
|
},
|
|
.erase_display_above => {
|
|
self.mode = .above;
|
|
self.protected = value;
|
|
},
|
|
.erase_display_complete => {
|
|
self.mode = .complete;
|
|
self.protected = value;
|
|
},
|
|
.erase_display_scrollback => {
|
|
self.mode = .scrollback;
|
|
self.protected = value;
|
|
},
|
|
.erase_display_scroll_complete => {
|
|
self.mode = .scroll_complete;
|
|
self.protected = value;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
{
|
|
for ("\x1B[?J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[?0J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[?1J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.above, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[?2J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.complete, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[?3J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
|
|
{
|
|
for ("\x1B[J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[0J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[1J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.above, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[2J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.complete, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[3J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
// Invalid and ignored by the handler
|
|
for ("\x1B[>0J") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
}
|
|
|
|
test "stream: DECEL, DECSEL" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
mode: ?csi.EraseLine = null,
|
|
protected: ?bool = null,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
switch (action) {
|
|
.erase_line_right => {
|
|
self.mode = .right;
|
|
self.protected = value;
|
|
},
|
|
.erase_line_left => {
|
|
self.mode = .left;
|
|
self.protected = value;
|
|
},
|
|
.erase_line_complete => {
|
|
self.mode = .complete;
|
|
self.protected = value;
|
|
},
|
|
.erase_line_right_unless_pending_wrap => {
|
|
self.mode = .right_unless_pending_wrap;
|
|
self.protected = value;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
{
|
|
for ("\x1B[?K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[?0K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[?1K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.left, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[?2K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?);
|
|
try testing.expect(s.handler.protected.?);
|
|
}
|
|
|
|
{
|
|
for ("\x1B[K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[0K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[1K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.left, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
for ("\x1B[2K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
{
|
|
// Invalid and ignored by the handler
|
|
for ("\x1B[<1K") |c| s.next(c);
|
|
try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?);
|
|
try testing.expect(!s.handler.protected.?);
|
|
}
|
|
}
|
|
|
|
test "stream: DECSCUSR" {
|
|
const H = struct {
|
|
style: ?ansi.CursorStyle = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Stream(@This()).Action.Tag,
|
|
value: Stream(@This()).Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.cursor_style => self.style = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[ q");
|
|
try testing.expect(s.handler.style.? == .default);
|
|
|
|
s.nextSlice("\x1B[1 q");
|
|
try testing.expect(s.handler.style.? == .blinking_block);
|
|
|
|
// Invalid and ignored by the handler
|
|
s.nextSlice("\x1B[?0 q");
|
|
try testing.expect(s.handler.style.? == .blinking_block);
|
|
}
|
|
|
|
test "stream: DECSCUSR without space" {
|
|
const H = struct {
|
|
style: ?ansi.CursorStyle = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Stream(@This()).Action.Tag,
|
|
value: Stream(@This()).Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.cursor_style => self.style = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[q");
|
|
try testing.expect(s.handler.style == null);
|
|
|
|
s.nextSlice("\x1B[1q");
|
|
try testing.expect(s.handler.style == null);
|
|
}
|
|
|
|
test "stream: XTSHIFTESCAPE" {
|
|
const H = struct {
|
|
escape: ?bool = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.mouse_shift_capture => self.escape = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[>2s");
|
|
try testing.expect(s.handler.escape == null);
|
|
|
|
s.nextSlice("\x1B[>s");
|
|
try testing.expect(s.handler.escape.? == false);
|
|
|
|
s.nextSlice("\x1B[>0s");
|
|
try testing.expect(s.handler.escape.? == false);
|
|
|
|
s.nextSlice("\x1B[>1s");
|
|
try testing.expect(s.handler.escape.? == true);
|
|
|
|
// Invalid and ignored by the handler
|
|
s.nextSlice("\x1B[1 s");
|
|
try testing.expect(s.handler.escape.? == true);
|
|
}
|
|
|
|
test "stream: change window title with invalid utf-8" {
|
|
const H = struct {
|
|
seen: bool = false,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
_ = value;
|
|
switch (action) {
|
|
.window_title => self.seen = true,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
{
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1b]2;abc\x1b\\");
|
|
try testing.expect(s.handler.seen);
|
|
}
|
|
|
|
{
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1b]2;abc\xc0\x1b\\");
|
|
try testing.expect(!s.handler.seen);
|
|
}
|
|
}
|
|
|
|
test "stream: insert characters" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
called: bool = false,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
_ = value;
|
|
switch (action) {
|
|
.insert_blanks => self.called = true,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
for ("\x1B[42@") |c| s.next(c);
|
|
try testing.expect(s.handler.called);
|
|
|
|
s.handler.called = false;
|
|
for ("\x1B[?42@") |c| s.next(c);
|
|
try testing.expect(!s.handler.called);
|
|
}
|
|
|
|
test "stream: insert characters explicit zero clamps to 1" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
value: ?usize = null,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
switch (action) {
|
|
.insert_blanks => self.value = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
for ("\x1B[0@") |c| s.next(c);
|
|
try testing.expectEqual(@as(usize, 1), s.handler.value.?);
|
|
}
|
|
|
|
test "stream: SCOSC" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
called: bool = false,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: Stream(Self).Action.Tag,
|
|
value: Stream(Self).Action.Value(action),
|
|
) void {
|
|
_ = value;
|
|
switch (action) {
|
|
.left_and_right_margin => @panic("bad"),
|
|
.left_and_right_margin_ambiguous => self.called = true,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
for ("\x1B[s") |c| s.next(c);
|
|
try testing.expect(s.handler.called);
|
|
}
|
|
|
|
test "stream: SCORC" {
|
|
const H = struct {
|
|
const Self = @This();
|
|
called: bool = false,
|
|
|
|
pub fn vt(
|
|
self: *Self,
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
_ = value;
|
|
switch (action) {
|
|
.restore_cursor => self.called = true,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
for ("\x1B[u") |c| s.next(c);
|
|
try testing.expect(s.handler.called);
|
|
}
|
|
|
|
test "stream: too many csi params" {
|
|
const H = struct {
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
_ = self;
|
|
_ = value;
|
|
switch (action) {
|
|
.cursor_right => unreachable,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1C");
|
|
}
|
|
|
|
test "stream: csi param too long" {
|
|
const H = struct {
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
_ = self;
|
|
_ = action;
|
|
_ = value;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
s.nextSlice("\x1B[1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111C");
|
|
}
|
|
|
|
test "stream: send report with CSI t" {
|
|
const H = struct {
|
|
style: ?csi.SizeReportStyle = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.size_report => self.style = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[14t");
|
|
try testing.expectEqual(csi.SizeReportStyle.csi_14_t, s.handler.style);
|
|
|
|
s.nextSlice("\x1b[16t");
|
|
try testing.expectEqual(csi.SizeReportStyle.csi_16_t, s.handler.style);
|
|
|
|
s.nextSlice("\x1b[18t");
|
|
try testing.expectEqual(csi.SizeReportStyle.csi_18_t, s.handler.style);
|
|
|
|
s.nextSlice("\x1b[21t");
|
|
try testing.expectEqual(csi.SizeReportStyle.csi_21_t, s.handler.style);
|
|
}
|
|
|
|
test "stream: invalid CSI t" {
|
|
const H = struct {
|
|
style: ?csi.SizeReportStyle = null,
|
|
|
|
pub fn sendSizeReport(self: *@This(), style: csi.SizeReportStyle) void {
|
|
self.style = style;
|
|
}
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
_ = self;
|
|
_ = action;
|
|
_ = value;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[19t");
|
|
try testing.expectEqual(null, s.handler.style);
|
|
}
|
|
|
|
test "stream: CSI t push title" {
|
|
const H = struct {
|
|
index: ?u16 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.title_push => self.index = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[22;0t");
|
|
try testing.expectEqual(@as(u16, 0), s.handler.index.?);
|
|
}
|
|
|
|
test "stream: CSI t push title with explicit window" {
|
|
const H = struct {
|
|
index: ?u16 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.title_push => self.index = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[22;2t");
|
|
try testing.expectEqual(@as(u16, 0), s.handler.index.?);
|
|
}
|
|
|
|
test "stream: CSI t push title with explicit icon" {
|
|
const H = struct {
|
|
index: ?u16 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.title_push => self.index = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[22;1t");
|
|
try testing.expectEqual(null, s.handler.index);
|
|
}
|
|
|
|
test "stream: CSI t push title with index" {
|
|
const H = struct {
|
|
index: ?u16 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.title_push => self.index = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[22;0;5t");
|
|
try testing.expectEqual(@as(u16, 5), s.handler.index.?);
|
|
}
|
|
|
|
test "stream: CSI t pop title" {
|
|
const H = struct {
|
|
index: ?u16 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.title_pop => self.index = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[23;0t");
|
|
try testing.expectEqual(@as(u16, 0), s.handler.index.?);
|
|
}
|
|
|
|
test "stream: CSI t pop title with explicit window" {
|
|
const H = struct {
|
|
index: ?u16 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.title_pop => self.index = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[23;2t");
|
|
try testing.expectEqual(@as(u16, 0), s.handler.index.?);
|
|
}
|
|
|
|
test "stream: CSI t pop title with explicit icon" {
|
|
const H = struct {
|
|
index: ?u16 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.title_pop => self.index = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[23;1t");
|
|
try testing.expectEqual(null, s.handler.index);
|
|
}
|
|
|
|
test "stream: CSI t pop title with index" {
|
|
const H = struct {
|
|
index: ?u16 = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: streampkg.Action.Tag,
|
|
value: streampkg.Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.title_pop => self.index = value,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[23;0;5t");
|
|
try testing.expectEqual(@as(u16, 5), s.handler.index.?);
|
|
}
|
|
|
|
test "stream CSI W clear tab stops" {
|
|
const H = struct {
|
|
action: ?Action.Key = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
_ = value;
|
|
self.action = action;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[2W");
|
|
try testing.expectEqual(Action.Key.tab_clear_current, s.handler.action.?);
|
|
|
|
s.nextSlice("\x1b[5W");
|
|
try testing.expectEqual(Action.Key.tab_clear_all, s.handler.action.?);
|
|
}
|
|
|
|
test "stream CSI W tab set" {
|
|
const H = struct {
|
|
action: ?Action.Key = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
_ = value;
|
|
self.action = action;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[W");
|
|
try testing.expectEqual(Action.Key.tab_set, s.handler.action.?);
|
|
|
|
s.handler.action = null;
|
|
s.nextSlice("\x1b[0W");
|
|
try testing.expectEqual(Action.Key.tab_set, s.handler.action.?);
|
|
|
|
s.handler.action = null;
|
|
s.nextSlice("\x1b[>W");
|
|
try testing.expect(s.handler.action == null);
|
|
|
|
s.handler.action = null;
|
|
s.nextSlice("\x1b[99W");
|
|
try testing.expect(s.handler.action == null);
|
|
}
|
|
|
|
test "stream CSI ? W reset tab stops" {
|
|
const H = struct {
|
|
action: ?Action.Key = null,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
_ = value;
|
|
self.action = action;
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
s.nextSlice("\x1b[?2W");
|
|
try testing.expect(s.handler.action == null);
|
|
|
|
s.nextSlice("\x1b[?5W");
|
|
try testing.expectEqual(Action.Key.tab_reset, s.handler.action.?);
|
|
|
|
// Invalid and ignored by the handler
|
|
s.handler.action = null;
|
|
s.nextSlice("\x1b[?1;2;3W");
|
|
try testing.expect(s.handler.action == null);
|
|
}
|
|
|
|
test "stream: SGR with 17+ parameters for underline color" {
|
|
const H = struct {
|
|
attrs: ?sgr.Attribute = null,
|
|
called: bool = false,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: anytype,
|
|
value: anytype,
|
|
) void {
|
|
switch (action) {
|
|
.set_attribute => {
|
|
self.attrs = value;
|
|
self.called = true;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
|
|
// Kakoune-style SGR with underline color as 17th parameter
|
|
// This tests the fix where param 17 was being dropped
|
|
s.nextSlice("\x1b[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136;0m");
|
|
try testing.expect(s.handler.called);
|
|
}
|
|
|
|
test "stream: tab clear with overflowing param" {
|
|
// Regression test for a fuzz crash: CSI with a parameter value that
|
|
// saturates to 65535 (u16 max) causes @enumFromInt to panic when
|
|
// converting to TabClear (enum(u8)).
|
|
const H = struct {
|
|
called: bool = false,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
_ = value;
|
|
switch (action) {
|
|
.tab_clear_current, .tab_clear_all => self.called = true,
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
var s: Stream(H) = .init(.{});
|
|
// This is the exact input from the fuzz crash (minus the mode byte):
|
|
// CSI with a huge numeric param that saturates to 65535, followed by 'g'.
|
|
s.nextSlice("\x1b[388888888888888888888888888888888888g\x1b[0m");
|
|
}
|
|
|
|
/// A test handler that accumulates APC bytes regardless of whether they
|
|
/// arrive per-byte (apc_put) or in bulk (apc_put_slice).
|
|
const ApcTestHandler = struct {
|
|
buf: [256]u8 = undefined,
|
|
len: usize = 0,
|
|
slices: usize = 0,
|
|
puts: usize = 0,
|
|
started: usize = 0,
|
|
ended: usize = 0,
|
|
|
|
pub fn vt(
|
|
self: *@This(),
|
|
comptime action: Action.Tag,
|
|
value: Action.Value(action),
|
|
) void {
|
|
switch (action) {
|
|
.apc_start => self.started += 1,
|
|
.apc_end => self.ended += 1,
|
|
.apc_put => {
|
|
self.buf[self.len] = value;
|
|
self.len += 1;
|
|
self.puts += 1;
|
|
},
|
|
.apc_put_slice => {
|
|
@memcpy(self.buf[self.len..][0..value.bytes.len], value.bytes);
|
|
self.len += value.bytes.len;
|
|
self.slices += 1;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
test "stream: apc bulk slice" {
|
|
var s: Stream(ApcTestHandler) = .init(.{});
|
|
s.nextSlice("\x1b_Gf=24,s=10,v=20;aGVsbG8=\x1b\\");
|
|
|
|
try testing.expectEqual(@as(usize, 1), s.handler.started);
|
|
try testing.expectEqual(@as(usize, 1), s.handler.ended);
|
|
try testing.expectEqualStrings(
|
|
"Gf=24,s=10,v=20;aGVsbG8=",
|
|
s.handler.buf[0..s.handler.len],
|
|
);
|
|
|
|
// With SIMD enabled the body must arrive as a single slice.
|
|
if (comptime build_options.simd and !debug) {
|
|
try testing.expectEqual(@as(usize, 1), s.handler.slices);
|
|
try testing.expectEqual(@as(usize, 0), s.handler.puts);
|
|
}
|
|
}
|
|
|
|
test "stream: apc bulk slice split across inputs" {
|
|
var s: Stream(ApcTestHandler) = .init(.{});
|
|
s.nextSlice("\x1b_Gf=24,s=10");
|
|
s.nextSlice(",v=20;aGVs");
|
|
s.nextSlice("bG8=\x1b\\");
|
|
|
|
try testing.expectEqual(@as(usize, 1), s.handler.started);
|
|
try testing.expectEqual(@as(usize, 1), s.handler.ended);
|
|
try testing.expectEqualStrings(
|
|
"Gf=24,s=10,v=20;aGVsbG8=",
|
|
s.handler.buf[0..s.handler.len],
|
|
);
|
|
}
|
|
|
|
test "stream: apc bulk slice keeps C0 bytes as data" {
|
|
var s: Stream(ApcTestHandler) = .init(.{});
|
|
// BEL does not terminate an APC string; it is payload data.
|
|
s.nextSlice("\x1b_Gx\x07y\x1b\\");
|
|
|
|
try testing.expectEqual(@as(usize, 1), s.handler.ended);
|
|
try testing.expectEqualStrings("Gx\x07y", s.handler.buf[0..s.handler.len]);
|
|
}
|
|
|
|
test "stream: apc aborted by CAN" {
|
|
var s: Stream(ApcTestHandler) = .init(.{});
|
|
// CAN (0x18) aborts the APC string via the anywhere => ground
|
|
// transition. Exiting the sos_pm_apc_string state emits apc_end,
|
|
// and the trailing bytes are printed, not treated as APC data.
|
|
s.nextSlice("\x1b_Gabcdefghijklmnopqrstuvwxyz0123456789\x18def");
|
|
|
|
try testing.expectEqual(@as(usize, 1), s.handler.started);
|
|
try testing.expectEqual(@as(usize, 1), s.handler.ended);
|
|
try testing.expectEqualStrings(
|
|
"Gabcdefghijklmnopqrstuvwxyz0123456789",
|
|
s.handler.buf[0..s.handler.len],
|
|
);
|
|
}
|
|
|
|
test "stream: apc scalar path matches" {
|
|
var s: Stream(ApcTestHandler) = .init(.{});
|
|
for ("\x1b_Gf=24;aGVsbG8=\x1b\\") |c| s.next(c);
|
|
|
|
try testing.expectEqual(@as(usize, 1), s.handler.started);
|
|
try testing.expectEqual(@as(usize, 1), s.handler.ended);
|
|
try testing.expectEqualStrings(
|
|
"Gf=24;aGVsbG8=",
|
|
s.handler.buf[0..s.handler.len],
|
|
);
|
|
}
|
|
|
|
test "stream: apc vector boundaries match scalar path" {
|
|
const positions = [_]usize{ 15, 16, 17, 31, 32, 33, 63, 64, 65 };
|
|
const controls = [_]u8{ 0x18, 0x1A, 0x1B, 0x80, 0xFF };
|
|
|
|
for (positions) |position| for (controls) |control| {
|
|
var input: [96]u8 = undefined;
|
|
input[0..3].* = "\x1b_G".*;
|
|
@memset(input[3 .. 3 + position], 'a');
|
|
input[3 + position] = control;
|
|
input[4 + position] = '\\';
|
|
const bytes = input[0 .. 5 + position];
|
|
|
|
var bulk: Stream(ApcTestHandler) = .init(.{});
|
|
bulk.nextSlice(bytes);
|
|
var scalar: Stream(ApcTestHandler) = .init(.{});
|
|
for (bytes) |byte| scalar.next(byte);
|
|
|
|
try testing.expectEqual(scalar.handler.started, bulk.handler.started);
|
|
try testing.expectEqual(scalar.handler.ended, bulk.handler.ended);
|
|
try testing.expectEqualStrings(
|
|
scalar.handler.buf[0..scalar.handler.len],
|
|
bulk.handler.buf[0..bulk.handler.len],
|
|
);
|
|
};
|
|
}
|