Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450const streampkg = @This();const std = @import("std");const build_options = @import("terminal_options");const assert = @import("../quirks.zig").inlineAssert;const testing = std.testing;const Allocator = std.mem.Allocator;const simd = @import("../simd/main.zig");const lib = @import("lib.zig");const Parser = @import("Parser.zig");const ansi = @import("ansi.zig");const charsets = @import("charsets.zig");const device_attributes = @import("device_attributes.zig");const device_status = @import("device_status.zig");const csi = @import("csi.zig");const kitty = @import("kitty.zig");const modes = @import("modes.zig");const osc = @import("osc.zig");const sgr = @import("sgr.zig");const UTF8Decoder = @import("UTF8Decoder.zig");const MouseShape = @import("mouse.zig").Shape;
const log = std.log.scoped(.stream);
/// Flip this to true when you want verbose debug output for/// debugging terminal stream issues. In addition to louder/// output this will also disable the SIMD optimizations in/// order to make it easier to see every byte. So if you're/// debugging an issue in the SIMD code then you'll need to/// do something else.const debug = false;
/// The possible actions that can be emitted by the Stream/// function for handling.pub const Action = union(Key) { print: Print, print_repeat: usize, bell, backspace, horizontal_tab: u16, horizontal_tab_back: u16, linefeed, carriage_return, enquiry, invoke_charset: InvokeCharset, cursor_up: CursorMovement, cursor_down: CursorMovement, cursor_left: CursorMovement, cursor_right: CursorMovement, cursor_col: CursorMovement, cursor_row: CursorMovement, cursor_col_relative: CursorMovement, cursor_row_relative: CursorMovement, cursor_pos: CursorPos, cursor_style: ansi.CursorStyle, erase_display_below: bool, erase_display_above: bool, erase_display_complete: bool, erase_display_scrollback: bool, erase_display_scroll_complete: bool, erase_line_right: bool, erase_line_left: bool, erase_line_complete: bool, erase_line_right_unless_pending_wrap: bool, delete_chars: usize, erase_chars: usize, insert_lines: usize, insert_blanks: usize, delete_lines: usize, scroll_up: usize, scroll_down: usize, tab_clear_current, tab_clear_all, tab_set, tab_reset, index, next_line, reverse_index, full_reset, set_mode: Mode, reset_mode: Mode, save_mode: Mode, restore_mode: Mode, request_mode: Mode, request_mode_unknown: RawMode, top_and_bottom_margin: Margin, left_and_right_margin: Margin, left_and_right_margin_ambiguous, save_cursor, restore_cursor, modify_key_format: ansi.ModifyKeyFormat, mouse_shift_capture: bool, protected_mode_off, protected_mode_iso, protected_mode_dec, size_report: csi.SizeReportStyle, title_push: u16, title_pop: u16, xtversion, device_attributes: device_attributes.Req, device_status: DeviceStatus, kitty_keyboard_query, kitty_keyboard_push: KittyKeyboardFlags, kitty_keyboard_pop: u16, kitty_keyboard_set: KittyKeyboardFlags, kitty_keyboard_set_or: KittyKeyboardFlags, kitty_keyboard_set_not: KittyKeyboardFlags, dcs_hook: Parser.Action.DCS, dcs_put: u8, dcs_unhook, apc_start, apc_end, apc_put: u8, end_hyperlink, active_status_display: ansi.StatusDisplay, decaln, window_title: WindowTitle, report_pwd: ReportPwd, show_desktop_notification: ShowDesktopNotification, progress_report: osc.Command.ProgressReport, start_hyperlink: StartHyperlink, clipboard_contents: ClipboardContents, mouse_shape: MouseShape, configure_charset: ConfigureCharset, set_attribute: sgr.Attribute, kitty_color_report: kitty.color.OSC, color_operation: ColorOperation, semantic_prompt: SemanticPrompt,
pub const Key = lib.Enum( lib.target, &.{ "print", "print_repeat", "bell", "backspace", "horizontal_tab", "horizontal_tab_back", "linefeed", "carriage_return", "enquiry", "invoke_charset", "cursor_up", "cursor_down", "cursor_left", "cursor_right", "cursor_col", "cursor_row", "cursor_col_relative", "cursor_row_relative", "cursor_pos", "cursor_style", "erase_display_below", "erase_display_above", "erase_display_complete", "erase_display_scrollback", "erase_display_scroll_complete", "erase_line_right", "erase_line_left", "erase_line_complete", "erase_line_right_unless_pending_wrap", "delete_chars", "erase_chars", "insert_lines", "insert_blanks", "delete_lines", "scroll_up", "scroll_down", "tab_clear_current", "tab_clear_all", "tab_set", "tab_reset", "index", "next_line", "reverse_index", "full_reset", "set_mode", "reset_mode", "save_mode", "restore_mode", "request_mode", "request_mode_unknown", "top_and_bottom_margin", "left_and_right_margin", "left_and_right_margin_ambiguous", "save_cursor", "restore_cursor", "modify_key_format", "mouse_shift_capture", "protected_mode_off", "protected_mode_iso", "protected_mode_dec", "size_report", "title_push", "title_pop", "xtversion", "device_attributes", "device_status", "kitty_keyboard_query", "kitty_keyboard_push", "kitty_keyboard_pop", "kitty_keyboard_set", "kitty_keyboard_set_or", "kitty_keyboard_set_not", "dcs_hook", "dcs_put", "dcs_unhook", "apc_start", "apc_end", "apc_put", "end_hyperlink", "active_status_display", "decaln", "window_title", "report_pwd", "show_desktop_notification", "progress_report", "start_hyperlink", "clipboard_contents", "mouse_shape", "configure_charset", "set_attribute", "kitty_color_report", "color_operation", "semantic_prompt", }, );
/// C ABI functions. const c_union = lib.TaggedUnion( lib.target, @This(), // TODO: Before shipping an ABI-compatible libghostty, verify this. // This was just arbitrarily chosen for now. [16]u64, ); pub const Tag = c_union.Tag; pub const Value = c_union.Value; pub const C = c_union.C; pub const CValue = c_union.CValue; pub const cval = c_union.cval;
/// Field types pub const Print = struct { cp: u21,
pub const C = extern struct { cp: u32, };
pub fn cval(self: Print) Print.C { return .{ .cp = @intCast(self.cp) }; } };
pub const InvokeCharset = lib.Struct(lib.target, struct { bank: charsets.ActiveSlot, charset: charsets.Slots, locking: bool, });
pub const CursorMovement = extern struct { /// The value of the cursor movement. Depending on the tag of this /// union this may be an absolute value or it may be a relative /// value. For example, `cursor_up` is relative, but `cursor_row` /// is absolute. value: u16, };
pub const CursorPos = extern struct { row: u16, col: u16, };
pub const DeviceStatus = struct { request: device_status.Request,
pub const C = u16;
pub fn cval(self: DeviceStatus) DeviceStatus.C { return @bitCast(self.request); } };
pub const Mode = struct { mode: modes.Mode,
pub const C = u16;
pub fn cval(self: Mode) Mode.C { return @bitCast(self.mode); } };
pub const RawMode = extern struct { mode: u16, ansi: bool, };
pub const Margin = extern struct { top_left: u16, bottom_right: u16, };
pub const KittyKeyboardFlags = struct { flags: kitty.KeyFlags,
pub const C = u8;
pub fn cval(self: KittyKeyboardFlags) KittyKeyboardFlags.C { return @intCast(self.flags.int()); } };
pub const WindowTitle = struct { title: []const u8,
pub const C = lib.String;
pub fn cval(self: WindowTitle) WindowTitle.C { return .init(self.title); } };
pub const ReportPwd = struct { url: []const u8,
pub const C = lib.String;
pub fn cval(self: ReportPwd) ReportPwd.C { return .init(self.url); } };
pub const ShowDesktopNotification = struct { title: []const u8, body: []const u8,
pub const C = extern struct { title: lib.String, body: lib.String, };
pub fn cval(self: ShowDesktopNotification) ShowDesktopNotification.C { return .{ .title = .init(self.title), .body = .init(self.body), }; } };
pub const StartHyperlink = struct { uri: []const u8, id: ?[]const u8,
pub const C = extern struct { uri: lib.String, id: lib.String, };
pub fn cval(self: StartHyperlink) StartHyperlink.C { return .{ .uri = .init(self.uri), .id = .init(self.id orelse ""), }; } };
pub const ClipboardContents = struct { kind: u8, data: []const u8,
pub const C = extern struct { kind: u8, data: lib.String, };
pub fn cval(self: ClipboardContents) ClipboardContents.C { return .{ .kind = self.kind, .data = .init(self.data), }; } };
pub const ConfigureCharset = lib.Struct(lib.target, struct { slot: charsets.Slots, charset: charsets.Charset, });
pub const ColorOperation = struct { op: osc.color.Operation, requests: osc.color.List, terminator: osc.Terminator,
pub const C = void;
pub fn cval(_: ColorOperation) ColorOperation.C { return {}; } };
pub const SemanticPrompt = osc.Command.SemanticPrompt;};
/// Returns a type that can process a stream of tty control characters./// This will call the `vt` function on type T with the following signature:////// fn(comptime action: Action.Key, value: Action.Value(action)) void////// The handler type T can choose to react to whatever actions it cares/// about in its pursuit of implementing a terminal emulator or other/// functionality.////// The Handler type must also have a `deinit` function.////// The "comptime" key is on purpose (vs. a standard Zig tagged union)/// because it allows the compiler to optimize away unimplemented actions./// e.g. you don't need to pay a conditional branching cost on every single/// action because the Zig compiler codegens separate code paths for every/// single action at comptime.pub fn Stream(comptime H: type) type { return struct { const Self = @This();
pub const Action = streampkg.Action; pub const Handler = H;
const T = switch (@typeInfo(Handler)) { .pointer => |p| p.child, else => Handler, };
handler: Handler, parser: Parser, utf8decoder: UTF8Decoder,
/// Initialize an allocation-free stream. This will preallocate various /// sizes as necessary and anything over that will be dropped. If you /// want to support more dynamic behavior use initAlloc instead. /// /// As a concrete example of something that requires heap allocation, /// consider OSC 52 (clipboard operations) which can be arbitrarily /// large. /// /// If you want to limit allocation size, use an allocator with /// a size limit with initAlloc. /// /// This takes ownership of the handler and will call deinit /// when the stream is deinitialized. pub fn init(h: Handler) Self { return .{ .handler = h, .parser = .init(), .utf8decoder = .{}, }; }
/// Initialize the stream that supports heap allocation as necessary. pub fn initAlloc(alloc: Allocator, h: Handler) Self { var self: Self = .init(h); self.parser.osc_parser.alloc = alloc; return self; }
pub fn deinit(self: *Self) void { self.parser.deinit(); self.handler.deinit(); }
/// Process a string of characters. pub inline fn nextSlice(self: *Self, input: []const u8) void { // Disable SIMD optimizations if build requests it or if our // manual debug mode is on. if (comptime debug or !build_options.simd) { for (input) |c| self.next(c); return; }
// This is the maximum number of codepoints we can decode // at one time for this function call. This is somewhat arbitrary // so if someone can demonstrate a better number then we can switch. var cp_buf: [4096]u32 = undefined;
// Split the input into chunks that fit into cp_buf. var i: usize = 0; while (true) { const len = @min(cp_buf.len, input.len - i); self.nextSliceCapped(input[i .. i + len], &cp_buf); i += len; if (i >= input.len) break; } }
inline fn nextSliceCapped( self: *Self, input: []const u8, cp_buf: []u32, ) void { assert(input.len <= cp_buf.len);
var offset: usize = 0;
// If the scalar UTF-8 decoder was in the middle of processing // a code sequence, we continue until it's not. while (self.utf8decoder.state != 0) { if (offset >= input.len) return; self.nextUtf8(input[offset]); offset += 1; } if (offset >= input.len) return;
// If we're not in the ground state then we process until // we are. This can happen if the last chunk of input put us // in the middle of a control sequence. offset += self.consumeUntilGround(input[offset..]); if (offset >= input.len) return; offset += self.consumeAllEscapes(input[offset..]);
// If we're in the ground state then we can use SIMD to process // input until we see an ESC (0x1B), since all other characters // up to that point are just UTF-8. while (self.parser.state == .ground and offset < input.len) { const res = simd.vt.utf8DecodeUntilControlSeq(input[offset..], cp_buf); for (cp_buf[0..res.decoded]) |cp| { if (cp <= 0xF) { self.execute(@intCast(cp)); } else { self.print(@intCast(cp)); } } // Consume the bytes we just processed. offset += res.consumed;
if (offset >= input.len) return;
// If our offset is NOT an escape then we must have a // partial UTF-8 sequence. In that case, we pass it off // to the scalar parser. if (input[offset] != 0x1B) { const rem = input[offset..]; for (rem) |c| self.nextUtf8(c); return; }
// Process control sequences until we run out. offset += self.consumeAllEscapes(input[offset..]); } }
/// Parses back-to-back escape sequences until none are left. /// Returns the number of bytes consumed from the provided input. /// /// Expects input to start with 0x1B, use consumeUntilGround first /// if the stream may be in the middle of an escape sequence. inline fn consumeAllEscapes(self: *Self, input: []const u8) usize { var offset: usize = 0; while (input[offset] == 0x1B) { self.parser.state = .escape; self.parser.clear(); offset += 1; offset += self.consumeUntilGround(input[offset..]); if (offset >= input.len) return input.len; } return offset; }
/// Parses escape sequences until the parser reaches the ground state. /// Returns the number of bytes consumed from the provided input. inline fn consumeUntilGround(self: *Self, input: []const u8) usize { var offset: usize = 0; while (self.parser.state != .ground) { if (offset >= input.len) return input.len; self.nextNonUtf8(input[offset]); offset += 1; } return offset; }
/// Like nextSlice but takes one byte and is necessarily a scalar /// operation that can't use SIMD. Prefer nextSlice if you can and /// try to get multiple bytes at once. pub inline fn next(self: *Self, c: u8) void { // The scalar path can be responsible for decoding UTF-8. if (self.parser.state == .ground) { self.nextUtf8(c); return; }
self.nextNonUtf8(c); }
/// Process the next byte and print as necessary. /// /// This assumes we're in the UTF-8 decoding state. If we may not /// be in the UTF-8 decoding state call nextSlice or next. inline fn nextUtf8(self: *Self, c: u8) void { assert(self.parser.state == .ground);
const res = self.utf8decoder.next(c); const consumed = res[1]; if (res[0]) |codepoint| { self.handleCodepoint(codepoint); } if (!consumed) { // We optimize for the scenario where the text being // printed in the terminal ISN'T full of ill-formed // UTF-8 sequences. @branchHint(.unlikely);
const retry = self.utf8decoder.next(c); // It should be impossible for the decoder // to not consume the byte twice in a row. assert(retry[1] == true); if (retry[0]) |codepoint| { self.handleCodepoint(codepoint); } } }
/// To be called whenever the utf-8 decoder produces a codepoint. /// /// This function is abstracted this way to handle the case where /// the decoder emits a 0x1B after rejecting an ill-formed sequence. inline fn handleCodepoint(self: *Self, c: u21) void { // We need to increase the eval branch limit because a lot of // tests end up running almost completely at comptime due to // a chain of inline functions. @setEvalBranchQuota(100_000);
// C0 control if (c <= 0xF) { @branchHint(.unlikely); self.execute(@intCast(c)); return; } // ESC if (c == 0x1B) { self.parser.state = .escape; self.parser.clear(); return; } self.print(@intCast(c)); }
/// Process the next character and call any callbacks if necessary. /// /// This assumes that we're not in the UTF-8 decoding state. If /// we may be in the UTF-8 decoding state call nextSlice or next. fn nextNonUtf8(self: *Self, c: u8) void { assert(self.parser.state != .ground);
// Fast path for CSI entry. if (self.parser.state == .escape and c == '[') { self.parser.state = .csi_entry; return; } // Fast path for CSI params. if (self.parser.state == .csi_param) csi_param: { // csi_param is the most common parser state // other than ground by a fairly wide margin. // // ref: https://github.com/qwerasd205/asciinema-stats @branchHint(.likely); switch (c) { // A C0 escape (yes, this is valid): 0x00...0x0F => self.execute(c), // We ignore C0 escapes > 0xF since execute // doesn't have processing for them anyway: 0x10...0x17, 0x19, 0x1C...0x1F => {}, // We don't currently have any handling for // 0x18 or 0x1A, but they should still move // the parser state to ground. 0x18, 0x1A => self.parser.state = .ground, // A parameter digit: '0'...'9' => if (self.parser.params_idx < Parser.MAX_PARAMS) { self.parser.param_acc *|= 10; self.parser.param_acc +|= c - '0'; // The parser's CSI param action uses param_acc_idx // to decide if there's a final param that needs to // be consumed or not, but it doesn't matter really // what it is as long as it's not 0. self.parser.param_acc_idx |= 1; }, // A parameter separator: ':', ';' => if (self.parser.params_idx < Parser.MAX_PARAMS) { self.parser.params[self.parser.params_idx] = self.parser.param_acc; if (c == ':') self.parser.params_sep.set(self.parser.params_idx); self.parser.params_idx += 1;
self.parser.param_acc = 0; self.parser.param_acc_idx = 0; }, // Explicitly ignored: 0x7F => {}, // Defer to the state machine to // handle any other characters: else => break :csi_param, } return; }
// We explicitly inline this call here for performance reasons. // // We do this rather than mark Parser.next as inline because doing // that causes weird behavior in some tests- I'm not sure if they // miscompile or it's just very counter-intuitive comptime stuff, // but regardless, this is the easy solution. const actions = @call(.always_inline, Parser.next, .{ &self.parser, c });
for (actions) |action_opt| { const action = action_opt orelse continue; if (comptime debug) log.info("action: {f}", .{action});
// A handler can expose this to get the raw action before // it is further parsed. If this returns `true` then we skip // processing ourselves. if (@hasDecl(T, "vtRaw")) { const skip = self.handler.vtRaw(action) catch |err| err: { log.warn("error handling action manually err={} action={f}", .{ err, action, }); // Always skip erroneous actions because we can't // be sure... break :err true; };
if (skip) continue; }
switch (action) { .print => |p| self.print(p), .execute => |code| self.execute(code), .csi_dispatch => |csi_action| self.csiDispatch(csi_action), .esc_dispatch => |esc| self.escDispatch(esc), .osc_dispatch => |cmd| self.oscDispatch(cmd), .dcs_hook => |dcs| self.handler.vt(.dcs_hook, dcs), .dcs_put => |code| self.handler.vt(.dcs_put, code), .dcs_unhook => self.handler.vt(.dcs_unhook, {}), .apc_start => self.handler.vt(.apc_start, {}), .apc_put => |code| self.handler.vt(.apc_put, code), .apc_end => self.handler.vt(.apc_end, {}), } } }
inline fn print(self: *Self, c: u21) void { self.handler.vt(.print, .{ .cp = c }); }
inline fn execute(self: *Self, c: u8) void { // If the character is > 0x7F, it's a C1 (8-bit) control, // which is strictly equivalent to `ESC` plus `c - 0x40`. if (c > 0x7F) { @branchHint(.unlikely); log.info("executing C1 0x{x} as ESC {c}", .{ c, c - 0x40 }); self.escDispatch(.{ .intermediates = &.{}, .final = c - 0x40, }); return; }
const c0: ansi.C0 = @enumFromInt(c); if (comptime debug) log.info("execute: {f}", .{c0}); switch (c0) { // We ignore SOH/STX: https://github.com/microsoft/terminal/issues/10786 .NUL, .SOH, .STX => {},
.ENQ => self.handler.vt(.enquiry, {}), .BEL => self.handler.vt(.bell, {}), .BS => self.handler.vt(.backspace, {}), .HT => self.handler.vt(.horizontal_tab, 1), .LF, .VT, .FF => self.handler.vt(.linefeed, {}), .CR => self.handler.vt(.carriage_return, {}), .SO => self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G1, .locking = false }), .SI => self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G0, .locking = false }),
else => log.warn("invalid C0 character, ignoring: 0x{x}", .{c}), } }
inline fn csiDispatch(self: *Self, input: Parser.Action.CSI) void { // The branch hints here are based on real world data // which indicates that the most common CSI finals are: // // 1. m // 2. H // 3. K // 4. A // 5. C // 6. X // 7. l // 8. h // 9. r // // Together, these 9 finals make up about 96% of all // CSI sequences encountered in real world scenarios. // // Additionally, within the prongs, unlikely branch // hints have been added to branches that deal with // invalid sequences/commands, this is in order to // optimize for the happy path where we're getting // valid data from the program we're running. // // ref: https://github.com/qwerasd205/asciinema-stats
switch (input.final) { // CUU - Cursor Up 'A', 'k' => { @branchHint(.likely); switch (input.intermediates.len) { 0 => self.handler.vt(.cursor_up, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @branchHint(.unlikely); log.warn("invalid cursor up command: {f}", .{input}); return; }, }, }),
else => log.warn( "ignoring unimplemented CSI A with intermediates: {s}", .{input.intermediates}, ), } },
// CUD - Cursor Down 'B' => switch (input.intermediates.len) { 0 => self.handler.vt(.cursor_down, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @branchHint(.unlikely); log.warn("invalid cursor down command: {f}", .{input}); return; }, }, }),
else => log.warn( "ignoring unimplemented CSI B with intermediates: {s}", .{input.intermediates}, ), },
// CUF - Cursor Right 'C' => { @branchHint(.likely); switch (input.intermediates.len) { 0 => self.handler.vt(.cursor_right, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @branchHint(.unlikely); log.warn("invalid cursor right command: {f}", .{input}); return; }, }, }),
else => log.warn( "ignoring unimplemented CSI C with intermediates: {s}", .{input.intermediates}, ), } },
// CUB - Cursor Left 'D', 'j' => switch (input.intermediates.len) { 0 => self.handler.vt(.cursor_left, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @branchHint(.unlikely); log.warn("invalid cursor left command: {f}", .{input}); return; }, }, }),
else => log.warn( "ignoring unimplemented CSI D with intermediates: {s}", .{input.intermediates}, ), },
// CNL - Cursor Next Line 'E' => switch (input.intermediates.len) { 0 => { self.handler.vt(.cursor_down, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @branchHint(.unlikely); log.warn("invalid cursor up command: {f}", .{input}); return; }, }, }); self.handler.vt(.carriage_return, {}); },
else => log.warn( "ignoring unimplemented CSI E with intermediates: {s}", .{input.intermediates}, ), },
// CPL - Cursor Previous Line 'F' => switch (input.intermediates.len) { 0 => { self.handler.vt(.cursor_up, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @branchHint(.unlikely); log.warn("invalid cursor down command: {f}", .{input}); return; }, }, }); self.handler.vt(.carriage_return, {}); },
else => log.warn( "ignoring unimplemented CSI F with intermediates: {s}", .{input.intermediates}, ), },
// HPA - Cursor Horizontal Position Absolute // TODO: test 'G', '`' => switch (input.intermediates.len) { 0 => self.handler.vt(.cursor_col, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @branchHint(.unlikely); log.warn("invalid HPA command: {f}", .{input}); return; }, }, }),
else => log.warn( "ignoring unimplemented CSI G with intermediates: {s}", .{input.intermediates}, ), },
// CUP - Set Cursor Position. // TODO: test 'H', 'f' => { @branchHint(.likely); switch (input.intermediates.len) { 0 => { const pos: streampkg.Action.CursorPos = switch (input.params.len) { 0 => .{ .row = 1, .col = 1 }, 1 => .{ .row = input.params[0], .col = 1 }, 2 => .{ .row = input.params[0], .col = input.params[1] }, else => { @branchHint(.unlikely); log.warn("invalid CUP command: {f}", .{input}); return; }, }; self.handler.vt(.cursor_pos, pos); },
else => log.warn( "ignoring unimplemented CSI H with intermediates: {s}", .{input.intermediates}, ), } },
// CHT - Cursor Horizontal Tabulation 'I' => switch (input.intermediates.len) { 0 => self.handler.vt(.horizontal_tab, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid horizontal tab command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI I with intermediates: {s}", .{input.intermediates}, ), },
// Erase Display 'J' => { const protected_: ?bool = switch (input.intermediates.len) { 0 => false, 1 => if (input.intermediates[0] == '?') true else null, else => null, };
const protected = protected_ orelse { log.warn("invalid erase display command: {f}", .{input}); return; };
const mode_: ?csi.EraseDisplay = switch (input.params.len) { 0 => .below, 1 => std.meta.intToEnum(csi.EraseDisplay, input.params[0]) catch null, else => null, };
const mode = mode_ orelse { log.warn("invalid erase display command: {f}", .{input}); return; };
switch (mode) { .below => self.handler.vt(.erase_display_below, protected), .above => self.handler.vt(.erase_display_above, protected), .complete => self.handler.vt(.erase_display_complete, protected), .scrollback => self.handler.vt(.erase_display_scrollback, protected), .scroll_complete => self.handler.vt(.erase_display_scroll_complete, protected), } },
// Erase Line 'K' => { @branchHint(.likely); const protected_: ?bool = switch (input.intermediates.len) { 0 => false, 1 => if (input.intermediates[0] == '?') true else null, else => null, };
const protected = protected_ orelse { @branchHint(.unlikely); log.warn("invalid erase line command: {f}", .{input}); return; };
const mode_: ?csi.EraseLine = switch (input.params.len) { 0 => .right, 1 => if (input.params[0] < 3) @enumFromInt(input.params[0]) else null, else => null, };
const mode = mode_ orelse { @branchHint(.unlikely); log.warn("invalid erase line command: {f}", .{input}); return; };
switch (mode) { .right => self.handler.vt(.erase_line_right, protected), .left => self.handler.vt(.erase_line_left, protected), .complete => self.handler.vt(.erase_line_complete, protected), .right_unless_pending_wrap => self.handler.vt(.erase_line_right_unless_pending_wrap, protected), _ => { @branchHint(.unlikely); log.warn("invalid erase line mode: {}", .{mode}); }, } },
// IL - Insert Lines // TODO: test 'L' => switch (input.intermediates.len) { 0 => self.handler.vt(.insert_lines, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid IL command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI L with intermediates: {s}", .{input.intermediates}, ), },
// DL - Delete Lines // TODO: test 'M' => switch (input.intermediates.len) { 0 => self.handler.vt(.delete_lines, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid DL command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI M with intermediates: {s}", .{input.intermediates}, ), },
// Delete Character (DCH) 'P' => switch (input.intermediates.len) { 0 => self.handler.vt(.delete_chars, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid delete characters command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI P with intermediates: {s}", .{input.intermediates}, ), },
// Scroll Up (SD)
'S' => switch (input.intermediates.len) { 0 => self.handler.vt(.scroll_up, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid scroll up command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI S with intermediates: {s}", .{input.intermediates}, ), },
// Scroll Down (SD) 'T' => switch (input.intermediates.len) { 0 => self.handler.vt(.scroll_down, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid scroll down command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI T with intermediates: {s}", .{input.intermediates}, ), },
// Cursor Tabulation Control 'W' => switch (input.intermediates.len) { 0 => { if (input.params.len == 0 or (input.params.len == 1 and input.params[0] == 0)) { self.handler.vt(.tab_set, {}); return; }
switch (input.params.len) { 0 => unreachable,
1 => switch (input.params[0]) { 0 => unreachable,
2 => self.handler.vt(.tab_clear_current, {}),
5 => self.handler.vt(.tab_clear_all, {}),
else => {}, },
else => {}, }
log.warn("invalid cursor tabulation control: {f}", .{input}); return; },
1 => if (input.intermediates[0] == '?' and input.params.len == 1 and input.params[0] == 5) { self.handler.vt(.tab_reset, {}); } else log.warn("invalid cursor tabulation control: {f}", .{input}),
else => log.warn( "ignoring unimplemented CSI W with intermediates: {s}", .{input.intermediates}, ), },
// Erase Characters (ECH) 'X' => { @branchHint(.likely); switch (input.intermediates.len) { 0 => self.handler.vt(.erase_chars, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { @branchHint(.unlikely); log.warn("invalid erase characters command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI X with intermediates: {s}", .{input.intermediates}, ), } },
// CHT - Cursor Horizontal Tabulation Back 'Z' => switch (input.intermediates.len) { 0 => self.handler.vt(.horizontal_tab_back, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid horizontal tab back command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI Z with intermediates: {s}", .{input.intermediates}, ), },
// HPR - Cursor Horizontal Position Relative 'a' => switch (input.intermediates.len) { 0 => self.handler.vt(.cursor_col_relative, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid HPR command: {f}", .{input}); return; }, }, }),
else => log.warn( "ignoring unimplemented CSI a with intermediates: {s}", .{input.intermediates}, ), },
// Repeat Previous Char (REP) 'b' => switch (input.intermediates.len) { 0 => self.handler.vt(.print_repeat, switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid print repeat command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI b with intermediates: {s}", .{input.intermediates}, ), },
// c - Device Attributes (DA1) 'c' => { const req: ?device_attributes.Req = switch (input.intermediates.len) { 0 => .primary, 1 => switch (input.intermediates[0]) { '>' => .secondary, '=' => .tertiary, else => null, }, else => null, };
if (req) |r| { self.handler.vt(.device_attributes, r); } else { log.warn("invalid device attributes command: {f}", .{input}); return; } },
// VPA - Cursor Vertical Position Absolute 'd' => switch (input.intermediates.len) { 0 => self.handler.vt(.cursor_row, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid VPA command: {f}", .{input}); return; }, }, }),
else => log.warn( "ignoring unimplemented CSI d with intermediates: {s}", .{input.intermediates}, ), },
// VPR - Cursor Vertical Position Relative 'e' => switch (input.intermediates.len) { 0 => self.handler.vt(.cursor_row_relative, .{ .value = switch (input.params.len) { 0 => 1, 1 => input.params[0], else => { log.warn("invalid VPR command: {f}", .{input}); return; }, }, }),
else => log.warn( "ignoring unimplemented CSI e with intermediates: {s}", .{input.intermediates}, ), },
// TBC - Tab Clear // TODO: test 'g' => switch (input.intermediates.len) { 0 => { const mode: csi.TabClear = switch (input.params.len) { 1 => std.meta.intToEnum(csi.TabClear, input.params[0]) catch { log.warn("invalid tab clear mode: {}", .{input.params[0]}); return; }, else => { log.warn("invalid tab clear command: {f}", .{input}); return; }, }; switch (mode) { .current => self.handler.vt(.tab_clear_current, {}), .all => self.handler.vt(.tab_clear_all, {}), _ => log.warn("unknown tab clear mode: {}", .{mode}), } },
else => log.warn( "ignoring unimplemented CSI g with intermediates: {s}", .{input.intermediates}, ), },
// SM - Set Mode 'h' => mode: { @branchHint(.likely); const ansi_mode = ansi: { if (input.intermediates.len == 0) break :ansi true; if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :ansi false;
log.warn("invalid set mode command: {f}", .{input}); break :mode; };
for (input.params) |mode_int| { if (modes.modeFromInt(mode_int, ansi_mode)) |mode| { self.handler.vt(.set_mode, .{ .mode = mode }); } else { log.warn("unimplemented mode: {}", .{mode_int}); } } },
// RM - Reset Mode 'l' => mode: { @branchHint(.likely); const ansi_mode = ansi: { if (input.intermediates.len == 0) break :ansi true; if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :ansi false;
log.warn("invalid set mode command: {f}", .{input}); break :mode; };
for (input.params) |mode_int| { if (modes.modeFromInt(mode_int, ansi_mode)) |mode| { self.handler.vt(.reset_mode, .{ .mode = mode }); } else { log.warn("unimplemented mode: {}", .{mode_int}); } } },
// SGR - Select Graphic Rendition 'm' => { @branchHint(.likely); switch (input.intermediates.len) { 0 => { // This is the most common case. @branchHint(.likely); // log.info("parse SGR params={any}", .{input.params}); var p: sgr.Parser = .{ .params = input.params, .params_sep = input.params_sep, }; while (p.next()) |attr| { // log.info("SGR attribute: {}", .{attr}); self.handler.vt(.set_attribute, attr); } },
1 => switch (input.intermediates[0]) { '>' => blk: { if (input.params.len == 0) { // Reset self.handler.vt(.modify_key_format, .legacy); break :blk; }
var format: ansi.ModifyKeyFormat = switch (input.params[0]) { 0 => .legacy, 1 => .cursor_keys, 2 => .function_keys, 4 => .other_keys_none, else => { @branchHint(.unlikely); log.warn("invalid setModifyKeyFormat: {f}", .{input}); break :blk; }, };
if (input.params.len > 2) { @branchHint(.unlikely); log.warn("invalid setModifyKeyFormat: {f}", .{input}); break :blk; }
if (input.params.len == 2) { switch (format) { // We don't support any of the subparams yet for these. .legacy => {}, .cursor_keys => {}, .function_keys => {},
// We only support the numeric form. .other_keys_none => switch (input.params[1]) { 2 => format = .other_keys_numeric, else => {}, }, .other_keys_numeric_except => {}, .other_keys_numeric => {}, } }
self.handler.vt(.modify_key_format, format); },
else => log.warn( "unknown CSI m with intermediate: {}", .{input.intermediates[0]}, ), },
else => { // Nothing, but I wanted a place to put this comment: // there are others forms of CSI m that have intermediates. // `vim --clean` uses `CSI ? 4 m` and I don't know what // that means. log.warn( "ignoring unimplemented CSI m with intermediates: {s}", .{input.intermediates}, ); }, } },
// TODO: test 'n' => { // Handle deviceStatusReport first if (input.intermediates.len == 0 or input.intermediates[0] == '?') { if (input.params.len != 1) { log.warn("invalid device status report command: {f}", .{input}); return; }
const question = question: { if (input.intermediates.len == 0) break :question false; if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :question true;
log.warn("invalid set mode command: {f}", .{input}); return; };
const req = device_status.reqFromInt(input.params[0], question) orelse { log.warn("invalid device status report command: {f}", .{input}); return; };
self.handler.vt(.device_status, .{ .request = req }); return; }
// Handle other forms of CSI n switch (input.intermediates.len) { 0 => unreachable, // handled above
1 => switch (input.intermediates[0]) { '>' => { // This isn't strictly correct. CSI > n has parameters that // control what exactly is being disabled. However, we // only support reverting back to modify other keys in // numeric except format. self.handler.vt(.modify_key_format, .other_keys_numeric_except); },
else => log.warn( "unknown CSI n with intermediate: {}", .{input.intermediates[0]}, ), },
else => log.warn( "ignoring unimplemented CSI n with intermediates: {s}", .{input.intermediates}, ), } },
// DECRQM - Request Mode 'p' => switch (input.intermediates.len) { 2 => decrqm: { const ansi_mode = ansi: { switch (input.intermediates.len) { 1 => if (input.intermediates[0] == '$') break :ansi true, 2 => if (input.intermediates[0] == '?' and input.intermediates[1] == '$') break :ansi false, else => {}, }
log.warn( "ignoring unimplemented CSI p with intermediates: {s}", .{input.intermediates}, ); break :decrqm; };
if (input.params.len != 1) { log.warn("invalid DECRQM command: {f}", .{input}); break :decrqm; }
const mode_raw = input.params[0]; const mode = modes.modeFromInt(mode_raw, ansi_mode); if (mode) |m| { self.handler.vt(.request_mode, .{ .mode = m }); } else { self.handler.vt(.request_mode_unknown, .{ .mode = mode_raw, .ansi = ansi_mode, }); } },
else => log.warn( "ignoring unimplemented CSI p with intermediates: {s}", .{input.intermediates}, ), },
'q' => switch (input.intermediates.len) { 1 => switch (input.intermediates[0]) { // DECSCUSR - Select Cursor Style // TODO: test ' ' => { const style: ansi.CursorStyle = switch (input.params.len) { 0 => .default, 1 => switch (input.params[0]) { 0 => .default, 1 => .blinking_block, 2 => .steady_block, 3 => .blinking_underline, 4 => .steady_underline, 5 => .blinking_bar, 6 => .steady_bar, else => { log.warn("invalid cursor style value: {}", .{input.params[0]}); return; }, }, else => { log.warn("invalid set cursor style command: {f}", .{input}); return; }, }; self.handler.vt(.cursor_style, style); },
// DECSCA '"' => { const mode_: ?ansi.ProtectedMode = switch (input.params.len) { else => null, 0 => .off, 1 => switch (input.params[0]) { 0, 2 => .off, 1 => .dec, else => null, }, };
const mode = mode_ orelse { log.warn("invalid set protected mode command: {f}", .{input}); return; };
switch (mode) { .off => self.handler.vt(.protected_mode_off, {}), .iso => self.handler.vt(.protected_mode_iso, {}), .dec => self.handler.vt(.protected_mode_dec, {}), } },
// XTVERSION '>' => self.handler.vt(.xtversion, {}), else => { log.warn( "ignoring unimplemented CSI q with intermediates: {s}", .{input.intermediates}, ); }, },
else => log.warn( "ignoring unimplemented CSI p with intermediates: {s}", .{input.intermediates}, ), },
'r' => { @branchHint(.likely); switch (input.intermediates.len) { // DECSTBM - Set Top and Bottom Margins 0 => switch (input.params.len) { 0 => self.handler.vt(.top_and_bottom_margin, .{ .top_left = 0, .bottom_right = 0 }), 1 => self.handler.vt(.top_and_bottom_margin, .{ .top_left = input.params[0], .bottom_right = 0 }), 2 => self.handler.vt(.top_and_bottom_margin, .{ .top_left = input.params[0], .bottom_right = input.params[1] }), else => { @branchHint(.unlikely); log.warn("invalid DECSTBM command: {f}", .{input}); }, },
1 => switch (input.intermediates[0]) { // Restore Mode '?' => { for (input.params) |mode_int| { if (modes.modeFromInt(mode_int, false)) |mode| { self.handler.vt(.restore_mode, .{ .mode = mode }); } else { log.warn( "unimplemented restore mode: {}", .{mode_int}, ); } } },
else => log.warn( "unknown CSI s with intermediate: {f}", .{input}, ), },
else => log.warn( "ignoring unimplemented CSI s with intermediates: {f}", .{input}, ), } },
's' => switch (input.intermediates.len) { // DECSLRM 0 => switch (input.params.len) { // CSI S is ambiguous with zero params so we defer // to our handler to do the proper logic. If mode 69 // is set, then we should invoke DECSLRM, otherwise // we should invoke SC. 0 => self.handler.vt(.left_and_right_margin_ambiguous, {}), 1 => self.handler.vt(.left_and_right_margin, .{ .top_left = input.params[0], .bottom_right = 0 }), 2 => self.handler.vt(.left_and_right_margin, .{ .top_left = input.params[0], .bottom_right = input.params[1] }), else => log.warn("invalid DECSLRM command: {f}", .{input}), },
1 => switch (input.intermediates[0]) { '?' => { for (input.params) |mode_int| { if (modes.modeFromInt(mode_int, false)) |mode| { self.handler.vt(.save_mode, .{ .mode = mode }); } else { log.warn( "unimplemented save mode: {}", .{mode_int}, ); } } },
// XTSHIFTESCAPE '>' => capture: { const capture = switch (input.params.len) { 0 => false, 1 => switch (input.params[0]) { 0 => false, 1 => true, else => { log.warn("invalid XTSHIFTESCAPE command: {f}", .{input}); break :capture; }, }, else => { log.warn("invalid XTSHIFTESCAPE command: {f}", .{input}); break :capture; }, };
self.handler.vt(.mouse_shift_capture, capture); },
else => log.warn( "unknown CSI s with intermediate: {f}", .{input}, ), },
else => log.warn( "ignoring unimplemented CSI s with intermediates: {f}", .{input}, ), },
// XTWINOPS 't' => switch (input.intermediates.len) { 0 => { if (input.params.len > 0) { switch (input.params[0]) { 14 => if (input.params.len == 1) { // report the text area size in pixels self.handler.vt(.size_report, .csi_14_t); } else log.warn( "ignoring CSI 14 t with extra parameters: {f}", .{input}, ), 16 => if (input.params.len == 1) { // report cell size in pixels self.handler.vt(.size_report, .csi_16_t); } else log.warn( "ignoring CSI 16 t with extra parameters: {f}", .{input}, ), 18 => if (input.params.len == 1) { // report screen size in characters self.handler.vt(.size_report, .csi_18_t); } else log.warn( "ignoring CSI 18 t with extra parameters: {f}", .{input}, ), 21 => if (input.params.len == 1) { // report window title self.handler.vt(.size_report, .csi_21_t); } else log.warn( "ignoring CSI 21 t with extra parameters: {f}", .{input}, ), inline 22, 23 => |number| if ((input.params.len == 2 or input.params.len == 3) and // we only support window title (input.params[1] == 0 or input.params[1] == 2)) { // push/pop title const index: u16 = if (input.params.len == 3) input.params[2] else 0; switch (number) { 22 => self.handler.vt(.title_push, index), 23 => self.handler.vt(.title_pop, index), else => @compileError("unreachable"), } } else log.warn( "ignoring CSI 22/23 t with extra parameters: {f}", .{input}, ), else => log.warn( "ignoring CSI t with unimplemented parameter: {f}", .{input}, ), } } else log.err( "ignoring CSI t with no parameters: {f}", .{input}, ); }, else => log.warn( "ignoring unimplemented CSI t with intermediates: {f}", .{input}, ), },
'u' => switch (input.intermediates.len) { 0 => self.handler.vt(.restore_cursor, {}),
// Kitty keyboard protocol 1 => switch (input.intermediates[0]) { '?' => self.handler.vt(.kitty_keyboard_query, {}),
'>' => push: { const flags: u5 = if (input.params.len == 1) std.math.cast(u5, input.params[0]) orelse { log.warn("invalid pushKittyKeyboard command: {f}", .{input}); break :push; } else 0;
self.handler.vt(.kitty_keyboard_push, .{ .flags = @as(kitty.KeyFlags, @bitCast(flags)) }); },
'<' => { const number: u16 = if (input.params.len == 1) input.params[0] else 1;
self.handler.vt(.kitty_keyboard_pop, number); },
'=' => set: { const flags: u5 = if (input.params.len >= 1) std.math.cast(u5, input.params[0]) orelse { log.warn("invalid setKittyKeyboard command: {f}", .{input}); break :set; } else 0;
const number: u16 = if (input.params.len >= 2) input.params[1] else 1;
const action_tag: streampkg.Action.Tag = switch (number) { 1 => .kitty_keyboard_set, 2 => .kitty_keyboard_set_or, 3 => .kitty_keyboard_set_not, else => { log.warn("invalid setKittyKeyboard command: {f}", .{input}); break :set; }, };
const kitty_flags: streampkg.Action.KittyKeyboardFlags = .{ .flags = @as(kitty.KeyFlags, @bitCast(flags)) }; switch (action_tag) { .kitty_keyboard_set => self.handler.vt(.kitty_keyboard_set, kitty_flags), .kitty_keyboard_set_or => self.handler.vt(.kitty_keyboard_set_or, kitty_flags), .kitty_keyboard_set_not => self.handler.vt(.kitty_keyboard_set_not, kitty_flags), else => unreachable, } },
else => log.warn( "unknown CSI s with intermediate: {f}", .{input}, ), },
else => log.warn( "ignoring unimplemented CSI u: {f}", .{input}, ), },
// ICH - Insert Blanks '@' => switch (input.intermediates.len) { 0 => self.handler.vt(.insert_blanks, switch (input.params.len) { 0 => 1, 1 => @max(1, input.params[0]), else => { @branchHint(.unlikely); log.warn("invalid ICH command: {f}", .{input}); return; }, }),
else => log.warn( "ignoring unimplemented CSI @: {f}", .{input}, ), },
// DECSASD - Select Active Status Display '}' => decsasd: { // Verify we're getting a DECSASD command if (input.intermediates.len != 1 or input.intermediates[0] != '$') { log.warn("unimplemented CSI callback: {f}", .{input}); break :decsasd; } if (input.params.len != 1) { log.warn("unimplemented CSI callback: {f}", .{input}); break :decsasd; }
const display: ansi.StatusDisplay = switch (input.params[0]) { 0 => .main, 1 => .status_line, else => { log.warn("unimplemented CSI callback: {f}", .{input}); break :decsasd; }, };
self.handler.vt(.active_status_display, display); },
else => log.warn("unimplemented CSI action: {f}", .{input}), } }
inline fn oscDispatch(self: *Self, cmd: osc.Command) void { // The branch hints here are based on real world data // which indicates that the most common OSC commands are: // // 1. hyperlink_end // 2. change_window_title // 3. change_window_icon // 4. hyperlink_start // 5. report_pwd // 6. color_operation // 7. semantic_prompt // // Together, these 7 commands make up about 96% of all // OSC commands encountered in real world scenarios. // // Additionally, within the prongs, unlikely branch // hints have been added to branches that deal with // invalid sequences/commands, this is in order to // optimize for the happy path where we're getting // valid data from the program we're running. // // ref: https://github.com/qwerasd205/asciinema-stats
switch (cmd) { .semantic_prompt => |sp| { @branchHint(.likely); self.handler.vt(.semantic_prompt, sp); },
.change_window_title => |title| { @branchHint(.likely); if (!std.unicode.utf8ValidateSlice(title)) { @branchHint(.unlikely); log.warn("change title request: invalid utf-8, ignoring request", .{}); return; }
self.handler.vt(.window_title, .{ .title = title }); },
.change_window_icon => |icon| { @branchHint(.likely); log.info("OSC 1 (change icon) received and ignored icon={s}", .{icon}); },
.clipboard_contents => |clip| { self.handler.vt(.clipboard_contents, .{ .kind = clip.kind, .data = clip.data, }); },
.report_pwd => |v| { @branchHint(.likely); self.handler.vt(.report_pwd, .{ .url = v.value }); },
.mouse_shape => |v| { const shape = MouseShape.fromString(v.value) orelse { @branchHint(.unlikely); log.warn("unknown cursor shape: {s}", .{v.value}); return; };
self.handler.vt(.mouse_shape, shape); },
.color_operation => |v| { @branchHint(.likely); self.handler.vt(.color_operation, .{ .op = v.op, .requests = v.requests, .terminator = v.terminator, }); },
.kitty_color_protocol => |v| { self.handler.vt(.kitty_color_report, v); },
.show_desktop_notification => |v| { self.handler.vt(.show_desktop_notification, .{ .title = v.title, .body = v.body, }); },
.hyperlink_start => |v| { @branchHint(.likely); self.handler.vt(.start_hyperlink, .{ .uri = v.uri, .id = v.id, }); },
.hyperlink_end => { @branchHint(.likely); self.handler.vt(.end_hyperlink, {}); },
.conemu_progress_report => |v| { self.handler.vt(.progress_report, v); },
.conemu_sleep, .conemu_show_message_box, .conemu_change_tab_title, .conemu_wait_input, .conemu_guimacro, .conemu_comment, .conemu_xterm_emulation, .conemu_output_environment_variable, .conemu_run_process, .kitty_text_sizing, .kitty_clipboard_protocol, .context_signal, => { log.debug("unimplemented OSC callback: {}", .{cmd}); },
.invalid => { @branchHint(.cold); // This is an invalid internal state, not an invalid OSC // string being parsed. We shouldn't see this. log.warn("invalid OSC, should never happen", .{}); }, } }
inline fn configureCharset( self: *Self, intermediates: []const u8, set: charsets.Charset, ) void { if (intermediates.len != 1) { log.warn("invalid charset intermediate: {any}", .{intermediates}); return; }
const slot: charsets.Slots = switch (intermediates[0]) { // TODO: support slots '-', '.', '/'
'(' => .G0, ')' => .G1, '*' => .G2, '+' => .G3, else => { @branchHint(.unlikely); log.warn("invalid charset intermediate: {any}", .{intermediates}); return; }, };
self.handler.vt(.configure_charset, .{ .slot = slot, .charset = set, }); }
inline fn escDispatch( self: *Self, action: Parser.Action.ESC, ) void { // The branch hints here are based on real world data // which indicates that the most common ESC finals are: // // 1. B // 2. \ // 3. 0 // 4. M // 5. 8 // 6. 7 // 7. > // 8. = // // Together, these 8 finals make up nearly 99% of all // ESC sequences encountered in real world scenarios. // // Additionally, within the prongs, unlikely branch // hints have been added to branches that deal with // invalid sequences/commands, this is in order to // optimize for the happy path where we're getting // valid data from the program we're running. // // ref: https://github.com/qwerasd205/asciinema-stats
switch (action.final) { // Charsets 'B' => { @branchHint(.likely); self.configureCharset(action.intermediates, .ascii); }, 'A' => self.configureCharset(action.intermediates, .british), '0' => { @branchHint(.likely); self.configureCharset(action.intermediates, .dec_special); },
// DECSC - Save Cursor '7' => { @branchHint(.likely); switch (action.intermediates.len) { 0 => self.handler.vt(.save_cursor, {}), else => { @branchHint(.unlikely); log.warn("invalid command: {f}", .{action}); return; }, } },
'8' => blk: { @branchHint(.likely); switch (action.intermediates.len) { // DECRC - Restore Cursor 0 => { self.handler.vt(.restore_cursor, {}); break :blk {}; },
1 => switch (action.intermediates[0]) { // DECALN - Fill Screen with E '#' => { self.handler.vt(.decaln, {}); break :blk {}; },
else => {}, },
else => {}, // fall through }
log.warn("unimplemented ESC action: {f}", .{action}); },
// IND - Index 'D' => switch (action.intermediates.len) { 0 => self.handler.vt(.index, {}), else => { @branchHint(.unlikely); log.warn("invalid index command: {f}", .{action}); return; }, },
// NEL - Next Line 'E' => switch (action.intermediates.len) { 0 => self.handler.vt(.next_line, {}), else => { @branchHint(.unlikely); log.warn("invalid next line command: {f}", .{action}); return; }, },
// HTS - Horizontal Tab Set 'H' => switch (action.intermediates.len) { 0 => self.handler.vt(.tab_set, {}), else => { @branchHint(.unlikely); log.warn("invalid tab set command: {f}", .{action}); return; }, },
// RI - Reverse Index 'M' => { @branchHint(.likely); switch (action.intermediates.len) { 0 => self.handler.vt(.reverse_index, {}), else => { @branchHint(.unlikely); log.warn("invalid reverse index command: {f}", .{action}); return; }, } },
// SS2 - Single Shift 2 'N' => switch (action.intermediates.len) { 0 => self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G2, .locking = true, }), else => { @branchHint(.unlikely); log.warn("invalid single shift 2 command: {f}", .{action}); return; }, },
// SS3 - Single Shift 3 'O' => switch (action.intermediates.len) { 0 => self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G3, .locking = true, }), else => { @branchHint(.unlikely); log.warn("invalid single shift 3 command: {f}", .{action}); return; }, },
// SPA - Start of Guarded Area 'V' => switch (action.intermediates.len) { 0 => self.handler.vt(.protected_mode_iso, {}), else => log.warn("unimplemented ESC callback: {f}", .{action}), },
// EPA - End of Guarded Area 'W' => switch (action.intermediates.len) { 0 => self.handler.vt(.protected_mode_off, {}), else => log.warn("unimplemented ESC callback: {f}", .{action}), },
// DECID 'Z' => if (action.intermediates.len == 0) { self.handler.vt(.device_attributes, .primary); } else log.warn("unimplemented ESC callback: {f}", .{action}),
// RIS - Full Reset 'c' => switch (action.intermediates.len) { 0 => self.handler.vt(.full_reset, {}), else => { log.warn("invalid full reset command: {f}", .{action}); return; }, },
// LS2 - Locking Shift 2 'n' => switch (action.intermediates.len) { 0 => self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G2, .locking = false, }), else => { @branchHint(.unlikely); log.warn("invalid single shift 2 command: {f}", .{action}); return; }, },
// LS3 - Locking Shift 3 'o' => switch (action.intermediates.len) { 0 => self.handler.vt(.invoke_charset, .{ .bank = .GL, .charset = .G3, .locking = false, }), else => { @branchHint(.unlikely); log.warn("invalid single shift 3 command: {f}", .{action}); return; }, },
// LS1R - Locking Shift 1 Right '~' => switch (action.intermediates.len) { 0 => self.handler.vt(.invoke_charset, .{ .bank = .GR, .charset = .G1, .locking = false, }), else => { @branchHint(.unlikely); log.warn("invalid locking shift 1 right command: {f}", .{action}); return; }, },
// LS2R - Locking Shift 2 Right '}' => switch (action.intermediates.len) { 0 => self.handler.vt(.invoke_charset, .{ .bank = .GR, .charset = .G2, .locking = false, }), else => { @branchHint(.unlikely); log.warn("invalid locking shift 2 right command: {f}", .{action}); return; }, },
// LS3R - Locking Shift 3 Right '|' => switch (action.intermediates.len) { 0 => self.handler.vt(.invoke_charset, .{ .bank = .GR, .charset = .G3, .locking = false, }), else => { @branchHint(.unlikely); log.warn("invalid locking shift 3 right command: {f}", .{action}); return; }, },
// Set application keypad mode '=' => { @branchHint(.likely); switch (action.intermediates.len) { 0 => self.handler.vt(.set_mode, .{ .mode = .keypad_keys }), else => log.warn("unimplemented setMode: {f}", .{action}), } },
// Reset application keypad mode '>' => { @branchHint(.likely); switch (action.intermediates.len) { 0 => self.handler.vt(.reset_mode, .{ .mode = .keypad_keys }), else => log.warn("unimplemented setMode: {f}", .{action}), } },
// Sets ST (string terminator). We don't have to do anything // because our parser always accepts ST. '\\' => { @branchHint(.likely); },
else => log.warn("unimplemented ESC action: {f}", .{action}), } } };}
test Action { // Forces the C type to be reified when the target is C, ensuring // all our types are C ABI compatible. _ = Action.C;}
test "stream: print" { const H = struct { c: ?u21 = 0,
pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { switch (action) { .print => self.c = value.cp, else => {}, } } };
var s: Stream(H) = .init(.{}); s.next('x'); try testing.expectEqual(@as(u21, 'x'), s.handler.c.?);}
test "simd: print invalid utf-8" { const H = struct { c: ?u21 = 0,
pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { switch (action) { .print => self.c = value.cp, else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice(&.{0xFF}); try testing.expectEqual(@as(u21, 0xFFFD), s.handler.c.?);}
test "simd: complete incomplete utf-8" { const H = struct { c: ?u21 = null,
pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { switch (action) { .print => self.c = value.cp, else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice(&.{0xE0}); // 3 byte try testing.expect(s.handler.c == null); s.nextSlice(&.{0xA0}); // still incomplete try testing.expect(s.handler.c == null); s.nextSlice(&.{0x80}); try testing.expectEqual(@as(u21, 0x800), s.handler.c.?);}
test "stream: cursor right (CUF)" { const H = struct { amount: u16 = 0,
pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { switch (action) { .cursor_right => self.amount = value.value, else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[C"); try testing.expectEqual(@as(u16, 1), s.handler.amount);
s.nextSlice("\x1B[5C"); try testing.expectEqual(@as(u16, 5), s.handler.amount);
s.handler.amount = 0; s.nextSlice("\x1B[5;4C"); try testing.expectEqual(@as(u16, 0), s.handler.amount);
s.handler.amount = 0; s.nextSlice("\x1b[?3C"); try testing.expectEqual(@as(u16, 0), s.handler.amount);}
test "stream: dec set mode (SM) and reset mode (RM)" { const H = struct { mode: modes.Mode = @as(modes.Mode, @enumFromInt(1)),
pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { switch (action) { .set_mode => self.mode = value.mode, .reset_mode => self.mode = @as(modes.Mode, @enumFromInt(1)), else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[?6h"); try testing.expectEqual(@as(modes.Mode, .origin), s.handler.mode);
s.nextSlice("\x1B[?6l"); try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode);
s.handler.mode = @as(modes.Mode, @enumFromInt(1)); s.nextSlice("\x1B[6 h"); try testing.expectEqual(@as(modes.Mode, @enumFromInt(1)), s.handler.mode);}
test "stream: ansi set mode (SM) and reset mode (RM)" { const H = struct { mode: ?modes.Mode = null,
pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { switch (action) { .set_mode => self.mode = value.mode, .reset_mode => self.mode = null, else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[4h"); try testing.expectEqual(@as(modes.Mode, .insert), s.handler.mode.?);
s.nextSlice("\x1B[4l"); try testing.expect(s.handler.mode == null);
s.handler.mode = null; s.nextSlice("\x1B[>5h"); try testing.expect(s.handler.mode == null);}
test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" { const H = struct { mode: ?modes.Mode = null,
pub fn setMode(self: *@This(), mode: modes.Mode, v: bool) !void { self.mode = null; if (v) self.mode = mode; }
pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { _ = self; _ = value; } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[6h"); try testing.expect(s.handler.mode == null);
s.nextSlice("\x1B[6l"); try testing.expect(s.handler.mode == null);}
test "stream: restore mode" { const H = struct { const Self = @This(); called: bool = false,
pub fn vt( self: *Self, comptime action: Stream(Self).Action.Tag, value: Stream(Self).Action.Value(action), ) void { _ = value; switch (action) { .top_and_bottom_margin => self.called = true, else => {}, } } };
var s: Stream(H) = .init(.{}); for ("\x1B[?42r") |c| s.next(c); try testing.expect(!s.handler.called);}
test "stream: pop kitty keyboard with no params defaults to 1" { const H = struct { const Self = @This(); n: u16 = 0,
pub fn vt( self: *Self, comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .kitty_keyboard_pop => self.n = value, else => {}, } } };
var s: Stream(H) = .init(.{}); for ("\x1B[<u") |c| s.next(c); try testing.expectEqual(@as(u16, 1), s.handler.n);}
test "stream: DECSCA" { const H = struct { const Self = @This(); v: ?ansi.ProtectedMode = null,
pub fn vt( self: *Self, comptime action: Stream(Self).Action.Tag, value: Stream(Self).Action.Value(action), ) void { _ = value; switch (action) { .protected_mode_off => self.v = .off, .protected_mode_iso => self.v = .iso, .protected_mode_dec => self.v = .dec, else => {}, } } };
var s: Stream(H) = .init(.{}); { for ("\x1B[\"q") |c| s.next(c); try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?); } { for ("\x1B[0\"q") |c| s.next(c); try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?); } { for ("\x1B[2\"q") |c| s.next(c); try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?); } { for ("\x1B[1\"q") |c| s.next(c); try testing.expectEqual(ansi.ProtectedMode.dec, s.handler.v.?); }}
test "stream: DECED, DECSED" { const H = struct { const Self = @This(); mode: ?csi.EraseDisplay = null, protected: ?bool = null,
pub fn vt( self: *Self, comptime action: anytype, value: anytype, ) void { switch (action) { .erase_display_below => { self.mode = .below; self.protected = value; }, .erase_display_above => { self.mode = .above; self.protected = value; }, .erase_display_complete => { self.mode = .complete; self.protected = value; }, .erase_display_scrollback => { self.mode = .scrollback; self.protected = value; }, .erase_display_scroll_complete => { self.mode = .scroll_complete; self.protected = value; }, else => {}, } } };
var s: Stream(H) = .init(.{}); { for ("\x1B[?J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?); try testing.expect(s.handler.protected.?); } { for ("\x1B[?0J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?); try testing.expect(s.handler.protected.?); } { for ("\x1B[?1J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.above, s.handler.mode.?); try testing.expect(s.handler.protected.?); } { for ("\x1B[?2J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.complete, s.handler.mode.?); try testing.expect(s.handler.protected.?); } { for ("\x1B[?3J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?); try testing.expect(s.handler.protected.?); }
{ for ("\x1B[J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { for ("\x1B[0J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { for ("\x1B[1J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.above, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { for ("\x1B[2J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.complete, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { for ("\x1B[3J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { // Invalid and ignored by the handler for ("\x1B[>0J") |c| s.next(c); try testing.expectEqual(csi.EraseDisplay.scrollback, s.handler.mode.?); try testing.expect(!s.handler.protected.?); }}
test "stream: DECEL, DECSEL" { const H = struct { const Self = @This(); mode: ?csi.EraseLine = null, protected: ?bool = null,
pub fn vt( self: *Self, comptime action: anytype, value: anytype, ) void { switch (action) { .erase_line_right => { self.mode = .right; self.protected = value; }, .erase_line_left => { self.mode = .left; self.protected = value; }, .erase_line_complete => { self.mode = .complete; self.protected = value; }, .erase_line_right_unless_pending_wrap => { self.mode = .right_unless_pending_wrap; self.protected = value; }, else => {}, } } };
var s: Stream(H) = .init(.{}); { for ("\x1B[?K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?); try testing.expect(s.handler.protected.?); } { for ("\x1B[?0K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?); try testing.expect(s.handler.protected.?); } { for ("\x1B[?1K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.left, s.handler.mode.?); try testing.expect(s.handler.protected.?); } { for ("\x1B[?2K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?); try testing.expect(s.handler.protected.?); }
{ for ("\x1B[K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { for ("\x1B[0K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { for ("\x1B[1K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.left, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { for ("\x1B[2K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?); try testing.expect(!s.handler.protected.?); } { // Invalid and ignored by the handler for ("\x1B[<1K") |c| s.next(c); try testing.expectEqual(csi.EraseLine.complete, s.handler.mode.?); try testing.expect(!s.handler.protected.?); }}
test "stream: DECSCUSR" { const H = struct { style: ?ansi.CursorStyle = null,
pub fn vt( self: *@This(), comptime action: Stream(@This()).Action.Tag, value: Stream(@This()).Action.Value(action), ) void { switch (action) { .cursor_style => self.style = value, else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[ q"); try testing.expect(s.handler.style.? == .default);
s.nextSlice("\x1B[1 q"); try testing.expect(s.handler.style.? == .blinking_block);
// Invalid and ignored by the handler s.nextSlice("\x1B[?0 q"); try testing.expect(s.handler.style.? == .blinking_block);}
test "stream: DECSCUSR without space" { const H = struct { style: ?ansi.CursorStyle = null,
pub fn vt( self: *@This(), comptime action: Stream(@This()).Action.Tag, value: Stream(@This()).Action.Value(action), ) void { switch (action) { .cursor_style => self.style = value, else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[q"); try testing.expect(s.handler.style == null);
s.nextSlice("\x1B[1q"); try testing.expect(s.handler.style == null);}
test "stream: XTSHIFTESCAPE" { const H = struct { escape: ?bool = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .mouse_shift_capture => self.escape = value, else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[>2s"); try testing.expect(s.handler.escape == null);
s.nextSlice("\x1B[>s"); try testing.expect(s.handler.escape.? == false);
s.nextSlice("\x1B[>0s"); try testing.expect(s.handler.escape.? == false);
s.nextSlice("\x1B[>1s"); try testing.expect(s.handler.escape.? == true);
// Invalid and ignored by the handler s.nextSlice("\x1B[1 s"); try testing.expect(s.handler.escape.? == true);}
test "stream: change window title with invalid utf-8" { const H = struct { seen: bool = false,
pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) void { _ = value; switch (action) { .window_title => self.seen = true, else => {}, } } };
{ var s: Stream(H) = .init(.{}); s.nextSlice("\x1b]2;abc\x1b\\"); try testing.expect(s.handler.seen); }
{ var s: Stream(H) = .init(.{}); s.nextSlice("\x1b]2;abc\xc0\x1b\\"); try testing.expect(!s.handler.seen); }}
test "stream: insert characters" { const H = struct { const Self = @This(); called: bool = false,
pub fn vt( self: *Self, comptime action: anytype, value: anytype, ) void { _ = value; switch (action) { .insert_blanks => self.called = true, else => {}, } } };
var s: Stream(H) = .init(.{}); for ("\x1B[42@") |c| s.next(c); try testing.expect(s.handler.called);
s.handler.called = false; for ("\x1B[?42@") |c| s.next(c); try testing.expect(!s.handler.called);}
test "stream: insert characters explicit zero clamps to 1" { const H = struct { const Self = @This(); value: ?usize = null,
pub fn vt( self: *Self, comptime action: anytype, value: anytype, ) void { switch (action) { .insert_blanks => self.value = value, else => {}, } } };
var s: Stream(H) = .init(.{}); for ("\x1B[0@") |c| s.next(c); try testing.expectEqual(@as(usize, 1), s.handler.value.?);}
test "stream: SCOSC" { const H = struct { const Self = @This(); called: bool = false,
pub fn vt( self: *Self, comptime action: Stream(Self).Action.Tag, value: Stream(Self).Action.Value(action), ) void { _ = value; switch (action) { .left_and_right_margin => @panic("bad"), .left_and_right_margin_ambiguous => self.called = true, else => {}, } } };
var s: Stream(H) = .init(.{}); for ("\x1B[s") |c| s.next(c); try testing.expect(s.handler.called);}
test "stream: SCORC" { const H = struct { const Self = @This(); called: bool = false,
pub fn vt( self: *Self, comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { _ = value; switch (action) { .restore_cursor => self.called = true, else => {}, } } };
var s: Stream(H) = .init(.{}); for ("\x1B[u") |c| s.next(c); try testing.expect(s.handler.called);}
test "stream: too many csi params" { const H = struct { pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) void { _ = self; _ = value; switch (action) { .cursor_right => unreachable, else => {}, } } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1C");}
test "stream: csi param too long" { const H = struct { pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) void { _ = self; _ = action; _ = value; } };
var s: Stream(H) = .init(.{}); s.nextSlice("\x1B[1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111C");}
test "stream: send report with CSI t" { const H = struct { style: ?csi.SizeReportStyle = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .size_report => self.style = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[14t"); try testing.expectEqual(csi.SizeReportStyle.csi_14_t, s.handler.style);
s.nextSlice("\x1b[16t"); try testing.expectEqual(csi.SizeReportStyle.csi_16_t, s.handler.style);
s.nextSlice("\x1b[18t"); try testing.expectEqual(csi.SizeReportStyle.csi_18_t, s.handler.style);
s.nextSlice("\x1b[21t"); try testing.expectEqual(csi.SizeReportStyle.csi_21_t, s.handler.style);}
test "stream: invalid CSI t" { const H = struct { style: ?csi.SizeReportStyle = null,
pub fn sendSizeReport(self: *@This(), style: csi.SizeReportStyle) void { self.style = style; }
pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) void { _ = self; _ = action; _ = value; } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[19t"); try testing.expectEqual(null, s.handler.style);}
test "stream: CSI t push title" { const H = struct { index: ?u16 = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .title_push => self.index = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[22;0t"); try testing.expectEqual(@as(u16, 0), s.handler.index.?);}
test "stream: CSI t push title with explicit window" { const H = struct { index: ?u16 = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .title_push => self.index = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[22;2t"); try testing.expectEqual(@as(u16, 0), s.handler.index.?);}
test "stream: CSI t push title with explicit icon" { const H = struct { index: ?u16 = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .title_push => self.index = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[22;1t"); try testing.expectEqual(null, s.handler.index);}
test "stream: CSI t push title with index" { const H = struct { index: ?u16 = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .title_push => self.index = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[22;0;5t"); try testing.expectEqual(@as(u16, 5), s.handler.index.?);}
test "stream: CSI t pop title" { const H = struct { index: ?u16 = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .title_pop => self.index = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[23;0t"); try testing.expectEqual(@as(u16, 0), s.handler.index.?);}
test "stream: CSI t pop title with explicit window" { const H = struct { index: ?u16 = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .title_pop => self.index = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[23;2t"); try testing.expectEqual(@as(u16, 0), s.handler.index.?);}
test "stream: CSI t pop title with explicit icon" { const H = struct { index: ?u16 = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .title_pop => self.index = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[23;1t"); try testing.expectEqual(null, s.handler.index);}
test "stream: CSI t pop title with index" { const H = struct { index: ?u16 = null,
pub fn vt( self: *@This(), comptime action: streampkg.Action.Tag, value: streampkg.Action.Value(action), ) void { switch (action) { .title_pop => self.index = value, else => {}, } } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[23;0;5t"); try testing.expectEqual(@as(u16, 5), s.handler.index.?);}
test "stream CSI W clear tab stops" { const H = struct { action: ?Action.Key = null,
pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) void { _ = value; self.action = action; } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[2W"); try testing.expectEqual(Action.Key.tab_clear_current, s.handler.action.?);
s.nextSlice("\x1b[5W"); try testing.expectEqual(Action.Key.tab_clear_all, s.handler.action.?);}
test "stream CSI W tab set" { const H = struct { action: ?Action.Key = null,
pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) void { _ = value; self.action = action; } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[W"); try testing.expectEqual(Action.Key.tab_set, s.handler.action.?);
s.handler.action = null; s.nextSlice("\x1b[0W"); try testing.expectEqual(Action.Key.tab_set, s.handler.action.?);
s.handler.action = null; s.nextSlice("\x1b[>W"); try testing.expect(s.handler.action == null);
s.handler.action = null; s.nextSlice("\x1b[99W"); try testing.expect(s.handler.action == null);}
test "stream CSI ? W reset tab stops" { const H = struct { action: ?Action.Key = null,
pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) void { _ = value; self.action = action; } };
var s: Stream(H) = .init(.{});
s.nextSlice("\x1b[?2W"); try testing.expect(s.handler.action == null);
s.nextSlice("\x1b[?5W"); try testing.expectEqual(Action.Key.tab_reset, s.handler.action.?);
// Invalid and ignored by the handler s.handler.action = null; s.nextSlice("\x1b[?1;2;3W"); try testing.expect(s.handler.action == null);}
test "stream: SGR with 17+ parameters for underline color" { const H = struct { attrs: ?sgr.Attribute = null, called: bool = false,
pub fn vt( self: *@This(), comptime action: anytype, value: anytype, ) void { switch (action) { .set_attribute => { self.attrs = value; self.called = true; }, else => {}, } } };
var s: Stream(H) = .init(.{});
// Kakoune-style SGR with underline color as 17th parameter // This tests the fix where param 17 was being dropped s.nextSlice("\x1b[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136;0m"); try testing.expect(s.handler.called);}
test "stream: tab clear with overflowing param" { // Regression test for a fuzz crash: CSI with a parameter value that // saturates to 65535 (u16 max) causes @enumFromInt to panic when // converting to TabClear (enum(u8)). const H = struct { called: bool = false,
pub fn vt( self: *@This(), comptime action: Action.Tag, value: Action.Value(action), ) void { _ = value; switch (action) { .tab_clear_current, .tab_clear_all => self.called = true, else => {}, } } };
var s: Stream(H) = .init(.{}); // This is the exact input from the fuzz crash (minus the mode byte): // CSI with a huge numeric param that saturates to 65535, followed by 'g'. s.nextSlice("\x1b[388888888888888888888888888888888888g\x1b[0m");}