aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/stream.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/stream.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/stream.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/stream.zig3449
1 files changed, 3449 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/stream.zig b/vendor/libghostty-vt/src/terminal/stream.zig
new file mode 100644
index 00000000..9771334f
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/stream.zig
@@ -0,0 +1,3449 @@
+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_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,
+ 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_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",
+ "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) };
+ }
+ };
+
+ 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.
+///
+/// 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);
+ for (cp_buf[0..res.decoded]) |cp| {
+ if (cp <= 0xF) {
+ self.execute(@intCast(cp));
+ } else {
+ self.print(@intCast(cp));
+ }
+ }
+ // 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;
+ self.nextNonUtf8(input[offset]);
+ offset += 1;
+ }
+ return offset;
+ }
+
+ /// 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;
+ }
+ // 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;
+ },
+ // Explicitly ignored:
+ 0x7F => {},
+ // Defer to the state machine to
+ // handle any other characters:
+ else => break :csi_param,
+ }
+ 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, {}),
+ }
+ }
+ }
+
+ 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 => log.warn("invalid C0 character, ignoring: 0x{x}", .{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 {
+ log.warn("invalid device attributes command: {f}", .{input});
+ 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 {
+ log.warn("unimplemented mode: {}", .{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 {
+ log.warn("unimplemented mode: {}", .{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 => log.warn(
+ "unknown CSI m with intermediate: {}",
+ .{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 log.warn(
+ "ignoring CSI 22/23 t with extra parameters: {f}",
+ .{input},
+ ),
+ else => log.warn(
+ "ignoring CSI t with unimplemented parameter: {f}",
+ .{input},
+ ),
+ }
+ } 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);
+ log.info("OSC 1 (change icon) received and ignored icon={s}", .{icon});
+ },
+
+ .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,
+ .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
+ }
+
+ log.warn("unimplemented ESC action: {f}", .{action});
+ },
+
+ // 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 => log.warn("unimplemented ESC action: {f}", .{action}),
+ }
+ }
+ };
+}
+
+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,
+ 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,
+ 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");
+}