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 continuationpkg = @import("stream_continuation.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: ApcEnd, 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 ApcEnd = extern struct { /// False when CAN, SUB, or another aborting transition ended the APC. terminated: bool, }; 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, continuation: ?continuationpkg.Tracker, pub const Options = struct { /// The handler initial value, must be set. handler: Handler, /// Allocator to use. If this is not set then the stream /// will be fully allocation free. There are some operations /// that will be dropped in this case such as OSC 52 clipboard /// ops. allocator: ?Allocator = null, /// Maximum size in bytes of the continuation suffix. If this is /// null or zero then continuation tracking is disabled. This is /// only applied when `allocator` is non-null; without an allocator /// continuation tracking is disabled. Feeding this continuation /// suffix into an equivalent stream at ground reconstructs the /// unfinished state without repeating committed terminal effects. /// Continuation tracking is only supported by TerminalStream. continuation_max_bytes: ?usize = null, }; /// Initialize a stream. Without an allocator, operations that require /// heap allocation are dropped. /// /// As a concrete example of something that requires heap allocation, /// consider OSC 52 (clipboard operations) which can be arbitrarily /// large. /// /// This takes ownership of the handler and will call deinit /// when the stream is deinitialized. pub fn init(options: Options) Self { // Initialize the parser var parser: Parser = .init(); if (options.allocator) |alloc| parser.osc_parser.alloc = alloc; // Initialize the continuation tracker if one is requested. var tracker: ?continuationpkg.Tracker = null; if (options.allocator) |alloc| { if (options.continuation_max_bytes) |max_bytes| { if (max_bytes > 0) tracker = .init(alloc, max_bytes); } } return .{ .handler = options.handler, .parser = parser, .utf8decoder = .{}, .continuation = tracker, }; } pub fn deinit(self: *Self) void { if (self.continuation) |*tracker| tracker.deinit(); self.parser.deinit(); self.handler.deinit(); } /// Write the current continuation suffix directly to a caller-owned /// writer. The caller must pause and serialize access to this Stream. pub fn writeContinuation( self: *const Self, writer: *std.Io.Writer, ) (std.Io.Writer.Error || error{ ContinuationDisabled, ContinuationUnavailable, })!void { const tracker = self.continuation orelse return error.ContinuationDisabled; if (tracker.broken) return error.ContinuationUnavailable; try tracker.write(writer); } /// True when no continuation suffix is needed to reproduce the /// stream's current parsing state. pub inline fn ground(self: *const Self) bool { // Parser ground alone is not sufficient because the UTF-8 // decoder may have some state. return self.parser.state == .ground and self.utf8decoder.state == 0; } /// Update the continuation suffix after one complete feed call. /// Must only be called when tracking is enabled. fn trackContinuation(self: *Self, input: []const u8) void { const tracker = &self.continuation.?; // If we're in a ground state, we have no continuation suffix // to track by definition. if (self.ground()) { tracker.reset(); return; } // Retain the part of this feed needed to replay the unfinished // state. When the parser is grounded here, the feed must have // ended inside a UTF-8 codepoint instead, because the ground // check above covers both state machines. tracker.append( if (self.parser.state != .ground) .vt else .utf8, input, ); } /// Process a string of characters. pub inline fn nextSlice(self: *Self, input: []const u8) void { self.nextSliceUntracked(input); // Continuation tracking is opt-in and this branch predicts // perfectly, so disabled streams pay nothing else here. if (self.continuation != null) self.trackContinuation(input); } inline fn nextSliceUntracked(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.nextUntracked(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; } // Find the end of the printable run. This is an // early-exit search loop that LLVM won't // auto-vectorize, and printable runs dominate real // input, so scan several codepoints at a time // manually (same idiom as the printSliceFill run // scan). var end = i + 1; scan: { if (simd.lanes(u32)) |lanes| { const V = @Vector(lanes, u32); const threshold: V = @splat(0xF); while (end + lanes <= cps.len) { const v: V = cps[end..][0..lanes].*; const gt = v > threshold; if (!@reduce(.And, gt)) { const bits: std.meta.Int(.unsigned, lanes) = @bitCast(gt); end += @ctz(~bits); break :scan; } end += lanes; } } 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; // Fast-path normal string termination. This matches // Parser.next's exit and entry actions while avoiding // the generic action loop for every completed APC. switch (input[offset]) { std.ascii.control_code.esc => { self.parser.clear(); self.parser.state = .escape; self.handler.vt(.apc_end, .{ .terminated = true }); offset += 1; continue; }, 0x9C => { self.parser.state = .ground; self.handler.vt(.apc_end, .{ .terminated = true }); offset += 1; continue; }, else => {}, } // Aborting transitions need the scalar path so the // handler can distinguish them from terminators. } } 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 { self.nextUntracked(c); if (self.continuation != null) self.trackContinuation(&.{c}); } inline fn nextUntracked(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(200_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, .{ .terminated = c == std.ascii.control_code.esc or c == 0x9C, }), } } } /// 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.enums.fromInt(csi.EraseDisplay, input.params[0]), 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.enums.fromInt(csi.TabClear, input.params[0]) orelse { 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); for ("\x1B[ self.v = .off, .protected_mode_iso => self.v = .iso, .protected_mode_dec => self.v = .dec, else => {}, } } }; var s: Stream(H) = .init(.{ .handler = .{} }); { 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(.{ .handler = .{} }); { 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(.{ .handler = .{} }); { 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); s.nextSlice("\x1b]2;abc\x1b\\"); try testing.expect(s.handler.seen); } { var s: Stream(H) = .init(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); // 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(.{ .handler = .{} }); // 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(.{ .handler = .{} }); 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 C1 ST" { var s: Stream(ApcTestHandler) = .init(.{ .handler = .{} }); s.nextSlice("\x1b_Gpayload\x9c"); try testing.expectEqual(@as(usize, 1), s.handler.started); try testing.expectEqual(@as(usize, 1), s.handler.ended); try testing.expectEqualStrings( "Gpayload", s.handler.buf[0..s.handler.len], ); } test "stream: apc bulk slice split across inputs" { var s: Stream(ApcTestHandler) = .init(.{ .handler = .{} }); 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(.{ .handler = .{} }); // 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(.{ .handler = .{} }); // 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(.{ .handler = .{} }); 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(.{ .handler = .{} }); bulk.nextSlice(bytes); var scalar: Stream(ApcTestHandler) = .init(.{ .handler = .{} }); 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], ); }; } const ContinuationTestHandler = struct { committed: usize = 0, apc_active: bool = false, apc_buf: [256]u8 = undefined, apc_len: usize = 0, dcs_active: bool = false, pub fn deinit(_: *@This()) void {} pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { switch (action) { .apc_start => self.apc_active = true, .apc_put => { self.apc_buf[self.apc_len] = value; self.apc_len += 1; }, .apc_put_slice => { @memcpy( self.apc_buf[self.apc_len..][0..value.bytes.len], value.bytes, ); self.apc_len += value.bytes.len; }, .dcs_hook => self.dcs_active = true, .dcs_put => {}, .apc_end => { self.apc_active = false; self.apc_len = 0; self.committed += 1; }, .dcs_unhook => { self.dcs_active = false; self.committed += 1; }, .print => self.committed += 1, .print_slice => self.committed += value.cps.len, .print_repeat => self.committed += value, else => self.committed += 1, } } }; const ContinuationNullHandler = struct { pub fn deinit(_: *@This()) void {} pub fn vt( _: *@This(), comptime _: Action.Tag, _: anytype, ) void {} }; test "stream: continuation lifecycle" { const S = Stream(ContinuationTestHandler); var disabled: S = .init(.{ .handler = .{} }); defer disabled.deinit(); var disabled_buf: [1]u8 = undefined; var disabled_writer: std.Io.Writer = .fixed(&disabled_buf); try testing.expectError( error.ContinuationDisabled, disabled.writeContinuation(&disabled_writer), ); var zero_capacity: S = .init(.{ .handler = .{}, .continuation_max_bytes = 0, }); defer zero_capacity.deinit(); var zero_capacity_buf: [1]u8 = undefined; var zero_capacity_writer: std.Io.Writer = .fixed(&zero_capacity_buf); try testing.expectError( error.ContinuationDisabled, zero_capacity.writeContinuation(&zero_capacity_writer), ); var no_allocator: S = .init(.{ .handler = .{}, .continuation_max_bytes = 64, }); defer no_allocator.deinit(); var no_allocator_buf: [1]u8 = undefined; var no_allocator_writer: std.Io.Writer = .fixed(&no_allocator_buf); try testing.expectError( error.ContinuationDisabled, no_allocator.writeContinuation(&no_allocator_writer), ); var tracked = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 64, }); defer tracked.deinit(); tracked.nextSlice("complete input"); var empty_buf: [1]u8 = undefined; var empty_writer: std.Io.Writer = .fixed(&empty_buf); try tracked.writeContinuation(&empty_writer); try testing.expectEqual(@as(usize, 0), empty_writer.end); tracked.nextSlice("\x1b["); var short_buf: [1]u8 = undefined; var short_writer: std.Io.Writer = .fixed(&short_buf); try testing.expectError( error.WriteFailed, tracked.writeContinuation(&short_writer), ); var failing = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0, }); var failing_stream = Stream(ContinuationNullHandler).init(.{ .allocator = failing.allocator(), .handler = .{}, .continuation_max_bytes = 64, }); defer failing_stream.deinit(); failing_stream.nextSlice("\x1b["); var unavailable_buf: [1]u8 = undefined; var unavailable_writer: std.Io.Writer = .fixed(&unavailable_buf); try testing.expectError( error.ContinuationUnavailable, failing_stream.writeContinuation(&unavailable_writer), ); } test "stream: continuation suffixes are replay safe" { const Case = struct { input: []const u8, expected: []const u8, }; const cases = [_]Case{ .{ .input = "text\x1b", .expected = "\x1b" }, .{ .input = "text\x1b[12;", .expected = "\x1b[12;" }, .{ .input = "text\x1b[1\x07;2", .expected = "\x1b[1;2" }, .{ .input = "text\x1b]2;hello", .expected = "\x1b]2;hello" }, .{ .input = "text\x1b_Gabc", .expected = "\x1b_Gabc" }, .{ .input = "text\x1bP+qabc", .expected = "\x1bP+qabc" }, .{ .input = "text\xE0\xA0\xF0", .expected = "\xF0" }, .{ .input = "text\x1b[12\x1b", .expected = "\x1b" }, .{ .input = "text\x1b[12\x9D2;title", .expected = "\x1b[12\x9D2;title", }, }; const S = Stream(ContinuationTestHandler); for (cases) |case| { var stream = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer stream.deinit(); stream.nextSlice(case.input); var buf: [1024]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try stream.writeContinuation(&writer); try testing.expectEqualStrings(case.expected, writer.buffered()); } } test "stream: continuation reconstructs every unfinished VT state" { const Case = struct { input: []const u8, state: Parser.State, }; const cases = [_]Case{ .{ .input = "\x1b", .state = .escape }, .{ .input = "\x1b(", .state = .escape_intermediate }, .{ .input = "\x1b[", .state = .csi_entry }, .{ .input = "\x1b[1", .state = .csi_param }, .{ .input = "\x1b[1$", .state = .csi_intermediate }, .{ .input = "\x1b[:", .state = .csi_ignore }, .{ .input = "\x1bP", .state = .dcs_entry }, .{ .input = "\x1bP1", .state = .dcs_param }, .{ .input = "\x1bP1$", .state = .dcs_intermediate }, .{ .input = "\x1bP1q", .state = .dcs_passthrough }, .{ .input = "\x1bP:", .state = .dcs_ignore }, .{ .input = "\x1b]2;title", .state = .osc_string }, .{ .input = "\x1b_Gpayload", .state = .sos_pm_apc_string }, }; const S = Stream(ContinuationTestHandler); for (cases) |case| { var source = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer source.deinit(); source.nextSlice(case.input); try testing.expectEqual(case.state, source.parser.state); var buf: [1024]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try source.writeContinuation(&writer); var restored = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer restored.deinit(); restored.nextSlice(writer.buffered()); try testing.expectEqual(@as(usize, 0), restored.handler.committed); try testing.expectEqual(source.parser.state, restored.parser.state); try testing.expectEqual(source.utf8decoder.state, restored.utf8decoder.state); } // Parser ground is still unfinished while the UTF-8 decoder is waiting // for the remaining bytes of a codepoint. var utf8 = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 4, }); defer utf8.deinit(); utf8.next(0xF0); try testing.expectEqual(Parser.State.ground, utf8.parser.state); try testing.expect(utf8.utf8decoder.state != 0); var utf8_buf: [4]u8 = undefined; var utf8_writer: std.Io.Writer = .fixed(&utf8_buf); try utf8.writeContinuation(&utf8_writer); try testing.expectEqualSlices(u8, &.{0xF0}, utf8_writer.buffered()); } test "stream: continuation is chunking-independent and idempotent" { const S = Stream(ContinuationTestHandler); const input = "committed\x1b[1\x07;2"; var bulk = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer bulk.deinit(); bulk.nextSlice(input); var scalar = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer scalar.deinit(); for (input) |c| scalar.next(c); var bulk_buf: [1024]u8 = undefined; var bulk_writer: std.Io.Writer = .fixed(&bulk_buf); try bulk.writeContinuation(&bulk_writer); var scalar_buf: [1024]u8 = undefined; var scalar_writer: std.Io.Writer = .fixed(&scalar_buf); try scalar.writeContinuation(&scalar_writer); try testing.expectEqualStrings( bulk_writer.buffered(), scalar_writer.buffered(), ); var restored = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer restored.deinit(); restored.nextSlice(bulk_writer.buffered()); try testing.expectEqual(@as(usize, 0), restored.handler.committed); var restored_buf: [1024]u8 = undefined; var restored_writer: std.Io.Writer = .fixed(&restored_buf); try restored.writeContinuation(&restored_writer); try testing.expectEqualStrings( bulk_writer.buffered(), restored_writer.buffered(), ); bulk.handler.committed = 0; restored.handler.committed = 0; bulk.nextSlice("mZ"); restored.next('m'); restored.next('Z'); try testing.expectEqual(bulk.handler.committed, restored.handler.committed); } test "stream: continuation rebuilds APC handler input" { const S = Stream(ContinuationTestHandler); var source = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer source.deinit(); source.nextSlice("committed\x1b_Gabc"); var continuation: [1024]u8 = undefined; var writer: std.Io.Writer = .fixed(&continuation); try source.writeContinuation(&writer); var restored = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer restored.deinit(); restored.nextSlice(writer.buffered()); try testing.expectEqual(@as(usize, 0), restored.handler.committed); try testing.expect(restored.handler.apc_active); try testing.expectEqualStrings( source.handler.apc_buf[0..source.handler.apc_len], restored.handler.apc_buf[0..restored.handler.apc_len], ); source.handler.committed = 0; restored.handler.committed = 0; source.nextSlice("\x1b\\"); restored.nextSlice("\x1b\\"); try testing.expectEqual(source.handler.committed, restored.handler.committed); try testing.expect(!source.handler.apc_active); try testing.expect(!restored.handler.apc_active); } test "stream: continuation cap and recovery" { const S = Stream(ContinuationTestHandler); // The raw feed exceeds the cap, but only the unfinished three-byte // CSI suffix is retained. var seeded = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 4, }); defer seeded.deinit(); seeded.nextSlice("committed text\x1b[1"); var seeded_buf: [4]u8 = undefined; var seeded_writer: std.Io.Writer = .fixed(&seeded_buf); try seeded.writeContinuation(&seeded_writer); try testing.expectEqualStrings("\x1b[1", seeded_writer.buffered()); var exceeded = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 4, }); defer exceeded.deinit(); exceeded.nextSlice("\x1b[123"); var unavailable_buf: [1]u8 = undefined; var unavailable_writer: std.Io.Writer = .fixed(&unavailable_buf); try testing.expectError( error.ContinuationUnavailable, exceeded.writeContinuation(&unavailable_writer), ); // Completing the CSI reaches ground and recovers without rebuilding the // Stream. A later unfinished sequence is tracked normally. exceeded.nextSlice("mtext\x1b["); var recovered_buf: [4]u8 = undefined; var recovered_writer: std.Io.Writer = .fixed(&recovered_buf); try exceeded.writeContinuation(&recovered_writer); try testing.expectEqualStrings("\x1b[", recovered_writer.buffered()); // A fresh ESC seed also recovers broken tracking even when the stream // never reaches ground: the ESC abandons the previous unfinished // state and everything after it is retained. exceeded.nextSlice("\x1b[123"); var reexceeded_buf: [1]u8 = undefined; var reexceeded_writer: std.Io.Writer = .fixed(&reexceeded_buf); try testing.expectError( error.ContinuationUnavailable, exceeded.writeContinuation(&reexceeded_writer), ); exceeded.nextSlice("\x1b]0;"); var seed_buf: [4]u8 = undefined; var seed_writer: std.Io.Writer = .fixed(&seed_buf); try exceeded.writeContinuation(&seed_writer); try testing.expectEqualStrings("\x1b]0;", seed_writer.buffered()); } test "stream: continuation spans multiple bulk feeds" { const S = Stream(ContinuationTestHandler); // An unfinished APC grows across feeds that contain no new seed. var apc = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer apc.deinit(); apc.nextSlice("text\x1b_Gab"); apc.nextSlice("cd"); apc.nextSlice("ef"); var apc_buf: [16]u8 = undefined; var apc_writer: std.Io.Writer = .fixed(&apc_buf); try apc.writeContinuation(&apc_writer); try testing.expectEqualStrings("\x1b_Gabcdef", apc_writer.buffered()); // An incomplete UTF-8 sequence grows across feeds of its // continuation bytes. var utf8 = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 1024, }); defer utf8.deinit(); utf8.nextSlice("text\xF0"); utf8.nextSlice("\x9F"); var utf8_buf: [4]u8 = undefined; var utf8_writer: std.Io.Writer = .fixed(&utf8_buf); try utf8.writeContinuation(&utf8_writer); try testing.expectEqualSlices(u8, "\xF0\x9F", utf8_writer.buffered()); // A later feed with its own seed drops everything retained earlier. utf8.nextSlice("\x98\x84 done \x1b[38;5"); var seed_buf: [8]u8 = undefined; var seed_writer: std.Io.Writer = .fixed(&seed_buf); try utf8.writeContinuation(&seed_writer); try testing.expectEqualStrings("\x1b[38;5", seed_writer.buffered()); } test "stream: continuation exact cap and large unfinished string" { const S = Stream(ContinuationNullHandler); var exact = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 5, }); defer exact.deinit(); exact.nextSlice("\x1b[123"); var exact_buf: [5]u8 = undefined; var exact_writer: std.Io.Writer = .fixed(&exact_buf); try exact.writeContinuation(&exact_writer); try testing.expectEqualStrings("\x1b[123", exact_writer.buffered()); const payload_len = 12 * 1024; const input = try testing.allocator.alloc(u8, payload_len); defer testing.allocator.free(input); input[0..3].* = "\x1b_G".*; @memset(input[3..], 'A'); var large = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = payload_len, }); defer large.deinit(); large.nextSlice(input); const large_buf = try testing.allocator.alloc(u8, payload_len); defer testing.allocator.free(large_buf); var large_writer: std.Io.Writer = .fixed(large_buf); try large.writeContinuation(&large_writer); try testing.expectEqualSlices(u8, input, large_writer.buffered()); } test "stream: continuation allocation failure recovers" { var failing = testing.FailingAllocator.init(testing.allocator, .{}); const alloc = failing.allocator(); const S = Stream(ContinuationNullHandler); var stream = S.init(.{ .allocator = alloc, .handler = .{}, .continuation_max_bytes = 16 * 1024, }); defer stream.deinit(); var input = try testing.allocator.alloc(u8, 12 * 1024); defer testing.allocator.free(input); input[0..2].* = "\x1b[".*; @memset(input[2..], '1'); failing.fail_index = failing.alloc_index; stream.nextSlice(input); var unavailable_buf: [1]u8 = undefined; var unavailable_writer: std.Io.Writer = .fixed(&unavailable_buf); try testing.expectError( error.ContinuationUnavailable, stream.writeContinuation(&unavailable_writer), ); failing.fail_index = std.math.maxInt(usize); stream.next('m'); var recovered_buf: [1]u8 = undefined; var recovered_writer: std.Io.Writer = .fixed(&recovered_buf); try stream.writeContinuation(&recovered_writer); try testing.expectEqual(@as(usize, 0), recovered_writer.end); } test "stream: continuation every-byte cuts preserve future behavior" { const corpora = [_][]const u8{ "plain \xF0\x9F\x98\x84 utf8", "bad \xE0\xA0\xF0\x9F\x98\x84 utf8", "\x1b[1\x07;2mstyled\x1b[0m", "\x1b]2;window title\x1b\\text", "\x1bP$qm\x1b\\text", "\x1b_Ga=q;payload\x1b\\text", "\x1b_25a1;s\x1b\\text", "\x1b]2;first\x1b\\\x1b_Gsecond", "\x1b[12\x9D2;title\x1b\\text", "\x1b[12\x18text\x1b[1\x1Atext", }; const S = Stream(ContinuationTestHandler); for (corpora) |corpus| for (0..corpus.len + 1) |cut| { var source = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 64 * 1024, }); defer source.deinit(); source.nextSlice(corpus[0..cut]); var continuation: [64 * 1024]u8 = undefined; var continuation_writer: std.Io.Writer = .fixed(&continuation); try source.writeContinuation(&continuation_writer); var restored = S.init(.{ .allocator = testing.allocator, .handler = .{}, .continuation_max_bytes = 64 * 1024, }); defer restored.deinit(); restored.nextSlice(continuation_writer.buffered()); try testing.expectEqual(@as(usize, 0), restored.handler.committed); try testing.expectEqual(source.handler.apc_active, restored.handler.apc_active); try testing.expectEqual(source.handler.dcs_active, restored.handler.dcs_active); if (source.handler.apc_active) { try testing.expectEqualStrings( source.handler.apc_buf[0..source.handler.apc_len], restored.handler.apc_buf[0..restored.handler.apc_len], ); } var reexport: [64 * 1024]u8 = undefined; var reexport_writer: std.Io.Writer = .fixed(&reexport); try restored.writeContinuation(&reexport_writer); try testing.expectEqualStrings( continuation_writer.buffered(), reexport_writer.buffered(), ); source.handler.committed = 0; restored.handler.committed = 0; source.nextSlice(corpus[cut..]); var offset = cut; var partition = cut +% corpus.len +% 1; while (offset < corpus.len) { partition = partition *% 1664525 +% 1013904223; const len = @min(1 + partition % 7, corpus.len - offset); restored.nextSlice(corpus[offset..][0..len]); offset += len; } try testing.expectEqual(source.handler.committed, restored.handler.committed); try testing.expectEqual(source.handler.apc_active, restored.handler.apc_active); try testing.expectEqual(source.handler.dcs_active, restored.handler.dcs_active); var source_final: [64]u8 = undefined; var source_final_writer: std.Io.Writer = .fixed(&source_final); try source.writeContinuation(&source_final_writer); var restored_final: [64]u8 = undefined; var restored_final_writer: std.Io.Writer = .fixed(&restored_final); try restored.writeContinuation(&restored_final_writer); try testing.expectEqualStrings( source_final_writer.buffered(), restored_final_writer.buffered(), ); }; }