Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512//! Bounded text-terminal engine shared by live terminal sessions, recovery,//! replay, and deterministic rendering tests. Graphical terminal protocols//! are intentionally outside this module's contract.
const std = @import("std");const display_width = @import("../shared/display_width.zig");const contracts = @import("contracts.zig");
const Allocator = std.mem.Allocator;
pub const checkpoint_schema_revision: u16 = 1;const checkpoint_magic = "FXTE";pub const max_reply_effects: usize = 16;pub const max_reply_effect_bytes: usize = 256;pub const max_reply_effect_total_bytes: usize = 4096;const max_csi_params: usize = 16;const max_csi_intermediates: usize = 2;const max_string_bytes: usize = 4096;const max_sync_bytes: usize = 1024 * 1024;const max_pool_entries: usize = 65_535;const max_hyperlink_pool_bytes: usize = 4 * 1024 * 1024;const max_combining_pool_bytes: usize = 4 * 1024 * 1024;
pub const FeedMode = enum { native_live, tmux_live, journal_replay,};
pub const ProtocolReply = struct { bytes: []u8,};
pub const FeedResult = struct { stats: FeedStats = .{}, replies: std.ArrayList(ProtocolReply) = .empty, reply_bytes: usize = 0,
pub fn deinit(self: *FeedResult, alloc: Allocator) void { for (self.replies.items) |reply| alloc.free(reply.bytes); self.replies.deinit(alloc); self.* = undefined; }};
pub const Color = union(enum) { default, /// Indexed palette colour (0..255) from `\x1b[3Nm`, `\x1b[4Nm`, /// `\x1b[9Nm`, `\x1b[10Nm`, or `\x1b[38;5;N` / `\x1b[48;5;N`. indexed: u8, /// Truecolor from `\x1b[38;2;R;G;Bm` / `\x1b[48;2;R;G;Bm`. rgb: struct { r: u8, g: u8, b: u8 },
pub fn eql(a: Color, b: Color) bool { return switch (a) { .default => b == .default, .indexed => |ai| switch (b) { .indexed => |bi| ai == bi, else => false, }, .rgb => |ar| switch (b) { .rgb => |br| ar.r == br.r and ar.g == br.g and ar.b == br.b, else => false, }, }; }};
pub const StyleFlags = packed struct(u8) { bold: bool = false, dim: bool = false, italic: bool = false, underline: bool = false, reverse: bool = false, strike: bool = false, _pad: u2 = 0,
pub fn eql(a: StyleFlags, b: StyleFlags) bool { return @as(u8, @bitCast(a)) == @as(u8, @bitCast(b)); }};
pub const Style = struct { fg: Color = .default, bg: Color = .default, flags: StyleFlags = .{}, /// Index into `Grid.hyperlink_pool`. `0` means no OSC 8 hyperlink; /// IDs `>= 1` map to a URL string the cell should be wrapped with /// when re-emitted by `diffBand`. IDs are grid-local; they survive /// `Grid.clone` because the pool is dup'd in lockstep. hyperlink_id: u32 = 0,
pub fn eql(a: Style, b: Style) bool { return a.fg.eql(b.fg) and a.bg.eql(b.bg) and a.flags.eql(b.flags) and a.hyperlink_id == b.hyperlink_id; }};
pub const Cell = struct { /// Unicode codepoint in this cell. Zero for the trailing half of a /// wide (double-width) cell so callers can tell single from wide. codepoint: u21 = ' ', width: u8 = 1, /// Index into `Grid.combining_suffix_pool`. `0` means the lead /// codepoint has no remaining display-unit UTF-8 bytes. combining_suffix_id: u32 = 0, /// Style applied to this cell. Blank cells from erasures inherit /// the current bg so a `\x1b[48;5;236m\x1b[K` clears into the /// configured background, matching real terminals. style: Style = .{},};
pub const FeedStats = struct { max_row_touched: u16 = 0, scrolled: bool = false, scroll_rows: u16 = 0,
fn touchRow(self: *FeedStats, row: u16) void { self.max_row_touched = @max(self.max_row_touched, row); }};
const State = enum { normal, escape, csi, osc, dcs,};
const SavedCursor = struct { row: u16, col: u16, pending_wrap: bool, style: Style, origin_mode: bool,};
const SavedScreen = struct { rows: u16, cols: u16, cells: []Cell, row_origin: u16, cursor_row: u16, cursor_col: u16, autowrap: bool, pending_wrap: bool, cursor_visible: bool, cursor_shape: contracts.CursorShape, cursor_blinking: bool, current_style: Style, active_hyperlink_params: []const u8, last_printable_idx: ?usize, scroll_top: u16, scroll_bottom: u16, origin_mode: bool, insert_mode: bool, saved_cursor: ?SavedCursor,
/// Take ownership of the grid's live screen state, including its /// `cells` and `active_hyperlink_params` allocations. fn capture(grid: Grid) SavedScreen { return .{ .rows = grid.rows, .cols = grid.cols, .cells = grid.cells, .row_origin = grid.row_origin, .cursor_row = grid.cursor_row, .cursor_col = grid.cursor_col, .autowrap = grid.autowrap, .pending_wrap = grid.pending_wrap, .cursor_visible = grid.cursor_visible, .cursor_shape = grid.cursor_shape, .cursor_blinking = grid.cursor_blinking, .current_style = grid.current_style, .active_hyperlink_params = grid.active_hyperlink_params, .last_printable_idx = grid.last_printable_idx, .scroll_top = grid.scroll_top, .scroll_bottom = grid.scroll_bottom, .origin_mode = grid.origin_mode, .insert_mode = grid.insert_mode, .saved_cursor = grid.saved_cursor, }; }
/// Hand the saved state back to the grid, transferring ownership /// of the allocations. The caller frees the grid's previous ones. fn restoreTo(saved: SavedScreen, grid: *Grid) void { grid.rows = saved.rows; grid.cols = saved.cols; grid.cells = saved.cells; grid.row_origin = saved.row_origin; grid.cursor_row = saved.cursor_row; grid.cursor_col = saved.cursor_col; grid.autowrap = saved.autowrap; grid.pending_wrap = saved.pending_wrap; grid.cursor_shape = saved.cursor_shape; grid.cursor_blinking = saved.cursor_blinking; grid.current_style = saved.current_style; grid.active_hyperlink_params = saved.active_hyperlink_params; grid.last_printable_idx = saved.last_printable_idx; grid.scroll_top = saved.scroll_top; grid.scroll_bottom = saved.scroll_bottom; grid.origin_mode = saved.origin_mode; grid.insert_mode = saved.insert_mode; grid.saved_cursor = saved.saved_cursor; }
/// Deep copy. New fields are carried automatically; only the two /// allocations need duplicating. fn dupe(saved: SavedScreen, alloc: Allocator) !SavedScreen { var copy = saved; copy.cells = try alloc.dupe(Cell, saved.cells); errdefer alloc.free(copy.cells); copy.active_hyperlink_params = if (saved.active_hyperlink_params.len > 0) try alloc.dupe(u8, saved.active_hyperlink_params) else &.{}; return copy; }};
inline fn failGrid(err: anytype) @TypeOf(err)!Grid { return @errorCast(failGridDynamic(err));}
noinline fn failGridDynamic(err: anyerror) anyerror!Grid { return err;}
test "grid failures preserve exact error types and identities" { const invalid = failGrid(error.InvalidGridSize); try std.testing.expect(@TypeOf(invalid) == error{InvalidGridSize}!Grid); try std.testing.expectError(error.InvalidGridSize, invalid); try std.testing.expectError(error.OutOfMemory, failGrid(error.OutOfMemory));}
pub const Grid = struct { alloc: Allocator, rows: u16, cols: u16, cells: []Cell, /// Physical row backing logical terminal row one. Scrolling rotates /// this origin instead of shifting the complete cell grid. row_origin: u16 = 0, /// 1-indexed to match ANSI. cursor_row: u16 = 1, cursor_col: u16 = 1, autowrap: bool = true, /// Lazy-wrap flag: set when the cursor fills the last column. The /// next printable write will consume the flag and wrap before /// writing. Any cursor-moving sequence (CR, LF, CUP, backspace, /// ...) clears it without wrapping. pending_wrap: bool = false, cursor_visible: bool = true, cursor_shape: contracts.CursorShape = .block, cursor_blinking: bool = true, scroll_top: u16, scroll_bottom: u16, origin_mode: bool = false, insert_mode: bool = false, bracketed_paste: bool = false, mouse_modes: u16 = 0, focus_tracking: bool = false, application_cursor_keys: bool = false, application_keypad: bool = false, keyboard_protocol: bool = false, tab_stops: []bool, sync_active: bool = false, sync_buffer: std.ArrayList(u8) = .empty, /// Controls whether DEC-2026 blocks apply atomically or immediately. /// Shadow grids disable buffering so clones include in-block clears; /// otherwise DECRST could erase content omitted from the subsequent diff. defer_sync_updates: bool = true, /// Active SGR state. Updated by `\x1b[Nm` and applied to any cell /// the cursor writes into (and to the bg of cells cleared by EL/ED). current_style: Style = .{},
state: State = .normal, /// CSI parameters accumulated from the digit/';' bytes after `\x1b[`. csi_params: [max_csi_params]u16 = [_]u16{0} ** max_csi_params, csi_param_count: u8 = 0, csi_has_digit: bool = false, csi_private: u8 = 0, csi_intermediates: [max_csi_intermediates]u8 = [_]u8{0} ** max_csi_intermediates, csi_intermediate_count: u8 = 0, osc_saw_esc: bool = false, /// Captured OSC payload bytes (between `\x1b]` and the terminator). /// Retained to interpret OSC 8 (hyperlink) sequences; other OSC /// codes are still ignored. osc_buffer: std.ArrayList(u8) = .empty, dcs_saw_esc: bool = false, dcs_buffer: std.ArrayList(u8) = .empty, utf8_buffer: [4]u8 = [_]u8{0} ** 4, utf8_len: u8 = 0, utf8_expected: u8 = 0, /// Allocator-owned URL strings indexed by `Style.hyperlink_id`. /// `id == 0` is the sentinel "no hyperlink" and is NOT stored here /// (lookup is `id - 1`). hyperlink_pool: std.ArrayList([]u8) = .empty, hyperlink_pool_bytes: usize = 0, /// Allocator-owned ordered UTF-8 suffixes indexed by /// `Cell.combining_suffix_id`. IDs are grid-local. combining_suffix_pool: std.ArrayList([]u8) = .empty, combining_pool_bytes: usize = 0, /// Exact OSC 8 parameter bytes for the currently active hyperlink. /// These are presentation state, not part of URI interning. active_hyperlink_params: []const u8 = &.{}, /// Normal terminal buffer state saved by DECSET ?1049. The current /// fields represent the alternate buffer while this is non-null. saved_normal_screen: ?SavedScreen = null, saved_cursor: ?SavedCursor = null, /// Lead cell written by the most recent printable input byte /// sequence. A following width-zero codepoint modifies this cell. last_printable_idx: ?usize = null, feed_stats: ?*FeedStats = null, feed_mode: FeedMode = .journal_replay, feed_result: ?*FeedResult = null,
pub fn init(alloc: Allocator, cols: u16, rows: u16) !Grid { if (cols == 0 or rows == 0) return failGrid(error.InvalidGridSize); const cell_count = std.math.mul( usize, @intCast(cols), @intCast(rows), ) catch return failGrid(error.InvalidGridSize); if (cell_count > contracts.max_render_cells) { return failGrid(error.InvalidGridSize); } const cells = try alloc.alloc(Cell, cell_count); errdefer alloc.free(cells); @memset(cells, .{}); const tab_stops = try alloc.alloc(bool, cols); initializeTabStops(tab_stops); return .{ .alloc = alloc, .rows = rows, .cols = cols, .cells = cells, .scroll_top = 1, .scroll_bottom = rows, .tab_stops = tab_stops, }; }
pub fn deinit(self: *Grid) void { if (self.saved_normal_screen) |saved| { self.alloc.free(saved.cells); if (saved.active_hyperlink_params.len > 0) { self.alloc.free(saved.active_hyperlink_params); } } self.alloc.free(self.cells); self.sync_buffer.deinit(self.alloc); self.osc_buffer.deinit(self.alloc); self.dcs_buffer.deinit(self.alloc); self.alloc.free(self.tab_stops); for (self.hyperlink_pool.items) |url| self.alloc.free(url); self.hyperlink_pool.deinit(self.alloc); for (self.combining_suffix_pool.items) |suffix| self.alloc.free(suffix); self.combining_suffix_pool.deinit(self.alloc); if (self.active_hyperlink_params.len > 0) { self.alloc.free(self.active_hyperlink_params); } }
/// Resize the grid. Keeps top-left content, clips anything outside /// the new bounds, fills any grown area with blanks. Matches what a /// real terminal does when the pane shrinks or grows — content is /// not auto-cleared, so the caller (fx) is responsible for /// repainting. pub fn resize(self: *Grid, cols: u16, rows: u16) !void { if (cols == 0 or rows == 0) return error.InvalidGridSize; if (cols == self.cols and rows == self.rows) return;
const cell_count = std.math.mul( usize, @intCast(cols), @intCast(rows), ) catch return error.InvalidGridSize; if (cell_count > contracts.max_render_cells) { return error.InvalidGridSize; }
const new_cells = try self.resizedCells( self.cells, self.cols, self.rows, self.row_origin, cols, rows, ); errdefer self.alloc.free(new_cells); const normal_cells = if (self.saved_normal_screen) |saved| try self.resizedCells( saved.cells, saved.cols, saved.rows, saved.row_origin, cols, rows, ) else null; errdefer if (normal_cells) |cells| self.alloc.free(cells); const new_tab_stops = try self.resizedTabStops(cols); errdefer self.alloc.free(new_tab_stops);
self.alloc.free(self.cells); self.alloc.free(self.tab_stops); self.cells = new_cells; self.tab_stops = new_tab_stops; self.cols = cols; self.rows = rows; self.row_origin = 0; if (self.cursor_row > rows) self.cursor_row = rows; if (self.cursor_col > cols) self.cursor_col = cols; self.last_printable_idx = null; self.scroll_top = 1; self.scroll_bottom = rows; self.origin_mode = false;
if (self.saved_normal_screen) |*saved| { self.alloc.free(saved.cells); saved.cells = normal_cells.?; saved.rows = rows; saved.cols = cols; saved.row_origin = 0; if (saved.cursor_row > rows) saved.cursor_row = rows; if (saved.cursor_col > cols) saved.cursor_col = cols; saved.last_printable_idx = null; saved.scroll_top = 1; saved.scroll_bottom = rows; saved.origin_mode = false; } }
fn resizedTabStops(self: Grid, cols: u16) ![]bool { const stops = try self.alloc.alloc(bool, cols); initializeTabStops(stops); @memcpy(stops[0..@min(stops.len, self.tab_stops.len)], self.tab_stops[0..@min(stops.len, self.tab_stops.len)]); return stops; }
fn resizedCells( self: *Grid, source: []const Cell, source_cols: u16, source_rows: u16, source_row_origin: u16, cols: u16, rows: u16, ) ![]Cell { const cells = try self.alloc.alloc(Cell, @as(usize, cols) * @as(usize, rows)); errdefer self.alloc.free(cells); @memset(cells, .{});
const copy_rows = @min(source_rows, rows); const copy_cols = @min(source_cols, cols); var row: u16 = 0; while (row < copy_rows) : (row += 1) { const source_physical_row = physicalRowIndex( source_row_origin, row, source_rows, ); const src_base: usize = source_physical_row * @as(usize, source_cols); const dst_base: usize = @as(usize, row) * @as(usize, cols); @memcpy(cells[dst_base .. dst_base + copy_cols], source[src_base .. src_base + copy_cols]); } repairWideCells(cells, cols, rows); return cells; }
fn rowBase(self: Grid, row: u16) usize { std.debug.assert(row >= 1 and row <= self.rows); const physical_row = physicalRowIndex( self.row_origin, row - 1, self.rows, ); return physical_row * @as(usize, self.cols); }
fn cellIndex(self: Grid, row: u16, col: u16) usize { std.debug.assert(col >= 1 and col <= self.cols); return self.rowBase(row) + @as(usize, col) - 1; }
fn physicalIndexForLogicalOffset(self: Grid, offset: usize) usize { std.debug.assert(offset < self.cells.len); const cols: usize = self.cols; const logical_row: u16 = @intCast(offset / cols); const col = offset % cols; const physical_row = physicalRowIndex( self.row_origin, logical_row, self.rows, ); return physical_row * cols + col; }
/// Feeds output through the emulator, preserving partial control sequences. /// DEC-2026 blocks are buffered until the matching DECRST when enabled. pub fn feed(self: *Grid, bytes: []const u8) !void { var result = try self.feedMode(bytes, .journal_replay); result.deinit(self.alloc); }
pub fn feedMode(self: *Grid, bytes: []const u8, mode: FeedMode) !FeedResult { var result = FeedResult{}; errdefer result.deinit(self.alloc); const previous_stats = self.feed_stats; const previous_mode = self.feed_mode; const previous_result = self.feed_result; self.feed_stats = &result.stats; self.feed_mode = mode; self.feed_result = &result; defer { self.feed_stats = previous_stats; self.feed_mode = previous_mode; self.feed_result = previous_result; } try self.feedBytes(bytes); return result; }
fn feedBytes(self: *Grid, bytes: []const u8) !void { var remaining: []const u8 = bytes; while (remaining.len > 0) { if (!self.defer_sync_updates or !self.sync_active) { const consumed = try self.feedDirect( remaining, self.defer_sync_updates, ); remaining = remaining[consumed..]; if (consumed == 0) return error.InvalidParserState; if (!self.defer_sync_updates or !self.sync_active) continue; if (remaining.len == 0) break; }
const reset = "\x1b[?2026l"; if (self.sync_buffer.items.len >= max_sync_bytes) { return error.SynchronizedUpdateTooLarge; } try self.sync_buffer.append(self.alloc, remaining[0]); remaining = remaining[1..]; if (!std.mem.endsWith(u8, self.sync_buffer.items, reset)) { continue; }
self.sync_buffer.shrinkRetainingCapacity( self.sync_buffer.items.len - reset.len, ); { const buffered = try self.alloc.dupe(u8, self.sync_buffer.items); defer self.alloc.free(buffered); self.sync_buffer.clearRetainingCapacity(); self.sync_active = false; const consumed = try self.feedDirect(buffered, false); if (consumed != buffered.len) return error.InvalidParserState; } } }
pub fn feedWithStats(self: *Grid, bytes: []const u8, stats: *FeedStats) !void { const previous_stats = self.feed_stats; const previous_mode = self.feed_mode; const previous_result = self.feed_result; self.feed_stats = stats; self.feed_mode = .journal_replay; self.feed_result = null; defer { self.feed_stats = previous_stats; self.feed_mode = previous_mode; self.feed_result = previous_result; } try self.feedBytes(bytes); }
fn feedDirect( self: *Grid, bytes: []const u8, stop_on_sync_start: bool, ) !usize { var i: usize = 0; while (i < bytes.len) { const b = bytes[i]; if (b == 0x18 or b == 0x1a) { self.cancelControlSequence(); i += 1; continue; } switch (self.state) { .normal => { if (self.utf8_len != 0) { const consumed = try self.completePendingUtf8(bytes[i..]); i += consumed; continue; } if (b == 0x1b) { self.last_printable_idx = null; self.state = .escape; i += 1; continue; } const expected = std.unicode.utf8ByteSequenceLength(b) catch 1; if (expected > 1 and i + expected > bytes.len) { const tail = bytes[i..]; @memcpy(self.utf8_buffer[0..tail.len], tail); self.utf8_len = @intCast(tail.len); self.utf8_expected = expected; return bytes.len; } i += try self.writeByte(bytes, i); }, .escape => { try self.dispatchEscape(b); i += 1; }, .csi => { if ((b == '?' or b == '>' or b == '<' or b == '=') and self.csi_param_count == 0 and !self.csi_has_digit and self.csi_intermediate_count == 0) { if (self.csi_private == 0) self.csi_private = b; i += 1; continue; } if (b >= '0' and b <= '9') { const slot = &self.csi_params[self.csi_param_count]; slot.* = slot.* *| 10 +| @as(u16, b - '0'); self.csi_has_digit = true; i += 1; continue; } if (b == ';' or b == ':') { if (self.csi_param_count + 1 >= self.csi_params.len) { return error.TooManyCsiParameters; } self.csi_param_count += 1; self.csi_has_digit = false; i += 1; continue; } if (b >= 0x20 and b <= 0x2f) { if (self.csi_intermediate_count >= self.csi_intermediates.len) { return error.TooManyCsiIntermediates; } self.csi_intermediates[self.csi_intermediate_count] = b; self.csi_intermediate_count += 1; i += 1; continue; } if (b < 0x40 or b > 0x7e) { self.cancelControlSequence(); i += 1; continue; } if (self.csi_has_digit or self.csi_param_count > 0) { self.csi_param_count += 1; } try self.dispatchCsi(b); self.state = .normal; i += 1; if (stop_on_sync_start and self.sync_active) return i; }, .osc => { if (b == 0x07) { try self.dispatchOscAndFinish(); } else if (b == 0x1b) { self.osc_saw_esc = true; } else if (self.osc_saw_esc and b == '\\') { try self.dispatchOscAndFinish(); } else { if (self.osc_saw_esc) { try appendBounded(&self.osc_buffer, self.alloc, 0x1b, max_string_bytes); self.osc_saw_esc = false; } try appendBounded(&self.osc_buffer, self.alloc, b, max_string_bytes); } i += 1; }, .dcs => { if (b == 0x1b) { self.dcs_saw_esc = true; } else if (self.dcs_saw_esc and b == '\\') { try self.dispatchDcsAndFinish(); } else { if (self.dcs_saw_esc) { try appendBounded(&self.dcs_buffer, self.alloc, 0x1b, max_string_bytes); self.dcs_saw_esc = false; } try appendBounded(&self.dcs_buffer, self.alloc, b, max_string_bytes); } i += 1; }, } } return i; }
fn dispatchEscape(self: *Grid, byte: u8) !void { switch (byte) { '[' => { self.resetCsi(); self.state = .csi; }, ']' => { self.osc_saw_esc = false; self.osc_buffer.clearRetainingCapacity(); self.state = .osc; }, 'P' => { self.dcs_saw_esc = false; self.dcs_buffer.clearRetainingCapacity(); self.state = .dcs; }, '7' => { self.saveCursor(); self.state = .normal; }, '8' => { self.restoreCursor(); self.state = .normal; }, 'D' => { self.pending_wrap = false; self.advanceRowOrScroll(); self.state = .normal; }, 'E' => { self.pending_wrap = false; self.cursor_col = 1; self.advanceRowOrScroll(); self.state = .normal; }, 'M' => { self.pending_wrap = false; self.reverseIndex(); self.state = .normal; }, 'H' => { self.tab_stops[self.cursor_col - 1] = true; self.state = .normal; }, '=' => { self.application_keypad = true; self.state = .normal; }, '>' => { self.application_keypad = false; self.state = .normal; }, 'c' => { self.resetTerminal(); self.state = .normal; }, '\\' => self.state = .normal, else => self.state = .normal, } }
fn resetCsi(self: *Grid) void { self.csi_params = [_]u16{0} ** max_csi_params; self.csi_param_count = 0; self.csi_has_digit = false; self.csi_private = 0; self.csi_intermediates = [_]u8{0} ** max_csi_intermediates; self.csi_intermediate_count = 0; }
fn completePendingUtf8(self: *Grid, bytes: []const u8) !usize { if (bytes.len == 0) return 0; if (bytes[0] & 0xc0 != 0x80) { self.utf8_len = 0; self.utf8_expected = 0; _ = try self.writeByte("\xef\xbf\xbd", 0); return 0; } const needed = self.utf8_expected - self.utf8_len; const count = @min(bytes.len, needed); @memcpy( self.utf8_buffer[self.utf8_len .. self.utf8_len + count], bytes[0..count], ); self.utf8_len += @intCast(count); if (self.utf8_len != self.utf8_expected) return count; const complete = self.utf8_buffer[0..self.utf8_len]; self.utf8_len = 0; self.utf8_expected = 0; if (!std.unicode.utf8ValidateSlice(complete)) { _ = try self.writeByte("\xef\xbf\xbd", 0); } else { _ = try self.writeByte(complete, 0); } return count; }
fn dispatchDcsAndFinish(self: *Grid) !void { defer self.cancelControlSequence(); const payload = self.dcs_buffer.items; if (!std.mem.startsWith(u8, payload, "$q")) return; const query = payload[2..]; if (std.mem.eql(u8, query, "m")) { try self.appendReply("\x1bP1$r0m\x1b\\"); } else if (std.mem.eql(u8, query, "r")) { var buffer: [64]u8 = undefined; const reply = try std.fmt.bufPrint( &buffer, "\x1bP1$r{d};{d}r\x1b\\", .{ self.scroll_top, self.scroll_bottom }, ); try self.appendReply(reply); } else { try self.appendReply("\x1bP0$r\x1b\\"); } }
fn appendReply(self: *Grid, bytes: []const u8) !void { if (self.feed_mode != .native_live) return; const result = self.feed_result orelse return error.InvalidFeedMode; if (bytes.len == 0 or bytes.len > max_reply_effect_bytes or result.replies.items.len >= max_reply_effects or result.reply_bytes > max_reply_effect_total_bytes - bytes.len) { return error.ReplyEffectCapacityExceeded; } const owned = try self.alloc.dupe(u8, bytes); errdefer self.alloc.free(owned); try result.replies.append(self.alloc, .{ .bytes = owned }); result.reply_bytes += bytes.len; }
fn cancelControlSequence(self: *Grid) void { self.state = .normal; self.resetCsi(); self.osc_saw_esc = false; self.osc_buffer.clearRetainingCapacity(); self.dcs_saw_esc = false; self.dcs_buffer.clearRetainingCapacity(); }
fn dispatchOscAndFinish(self: *Grid) !void { defer self.cancelControlSequence(); try self.dispatchOsc(); }
/// Handles terminated OSC 8 payloads and ignores other OSC codes. /// Empty URIs close the active link; non-empty URIs open one. fn dispatchOsc(self: *Grid) !void { const payload = self.osc_buffer.items; if (payload.len < 2 or payload[0] != '8' or payload[1] != ';') return; // Skip params section (between the two `;`). var i: usize = 2; while (i < payload.len and payload[i] != ';') : (i += 1) {} if (i >= payload.len) return; const params = payload[2..i]; const uri = payload[i + 1 ..]; if (uri.len == 0) { self.clearActiveHyperlink(); return; }
const owned_params = if (params.len > 0) try self.alloc.dupe(u8, params) else &.{}; errdefer if (owned_params.len > 0) self.alloc.free(owned_params);
// Reuse an existing pool entry when the same URL appears // again so cells under one logical link share an id. for (self.hyperlink_pool.items, 0..) |existing, idx| { if (std.mem.eql(u8, existing, uri)) { self.replaceActiveHyperlinkParams(owned_params); self.current_style.hyperlink_id = @intCast(idx + 1); return; } } if (uri.len > max_string_bytes or self.hyperlink_pool.items.len >= max_pool_entries or self.hyperlink_pool_bytes > max_hyperlink_pool_bytes - uri.len) { return error.HyperlinkPoolCapacityExceeded; } const dup = try self.alloc.dupe(u8, uri); errdefer self.alloc.free(dup); try self.hyperlink_pool.ensureUnusedCapacity(self.alloc, 1); self.hyperlink_pool.appendAssumeCapacity(dup); self.hyperlink_pool_bytes += dup.len; self.replaceActiveHyperlinkParams(owned_params); self.current_style.hyperlink_id = @intCast(self.hyperlink_pool.items.len); }
fn clearActiveHyperlink(self: *Grid) void { self.replaceActiveHyperlinkParams(&.{}); self.current_style.hyperlink_id = 0; }
fn replaceActiveHyperlinkParams(self: *Grid, params: []const u8) void { if (self.active_hyperlink_params.len > 0) { self.alloc.free(self.active_hyperlink_params); } self.active_hyperlink_params = params; }
/// Look up the URL string for a `Style.hyperlink_id`, or `null` for /// the sentinel zero id. pub fn hyperlinkUrl(self: Grid, id: u32) ?[]const u8 { if (id == 0) return null; if (id - 1 >= self.hyperlink_pool.items.len) return null; return self.hyperlink_pool.items[id - 1]; }
pub fn combiningSuffix(self: Grid, id: u32) ?[]const u8 { if (id == 0) return null; if (id - 1 >= self.combining_suffix_pool.items.len) return null; return self.combining_suffix_pool.items[id - 1]; }
fn appendCombiningSuffix(self: *Grid, cell_idx: usize, bytes: []const u8) !void { const existing = self.combiningSuffix(self.cells[cell_idx].combining_suffix_id) orelse &.{}; const combined_len = std.math.add(usize, existing.len, bytes.len) catch return error.CombiningPoolCapacityExceeded; for (self.combining_suffix_pool.items, 0..) |candidate, idx| { if (candidate.len == combined_len and std.mem.startsWith(u8, candidate, existing) and std.mem.eql(u8, candidate[existing.len..], bytes)) { self.cells[cell_idx].combining_suffix_id = @intCast(idx + 1); return; } } if (combined_len > contracts.max_cell_text_bytes or self.combining_suffix_pool.items.len >= max_pool_entries or self.combining_pool_bytes > max_combining_pool_bytes - combined_len) { return error.CombiningPoolCapacityExceeded; }
const combined = try self.alloc.alloc(u8, combined_len); errdefer self.alloc.free(combined); @memcpy(combined[0..existing.len], existing); @memcpy(combined[existing.len..], bytes); try self.combining_suffix_pool.append(self.alloc, combined); self.combining_pool_bytes += combined.len; self.cells[cell_idx].combining_suffix_id = @intCast(self.combining_suffix_pool.items.len); }
pub fn atControlSequenceBoundary(self: Grid) bool { return self.state == .normal and !self.sync_active; }
pub fn restorePresentationBoundary( self: *Grid, resume_bytes: []const u8, cursor_col: u16, pending_wrap: bool, ) !void { std.debug.assert(self.atControlSequenceBoundary()); try self.feed(resume_bytes); if (!self.atControlSequenceBoundary()) return error.InvalidTranscriptTransition; self.cursor_col = @min(@max(cursor_col, 1), self.cols); self.pending_wrap = pending_wrap; }
pub fn writePresentationResume(self: Grid, out: *std.Io.Writer) !void { var presentation = self.current_style; const hyperlink_id = presentation.hyperlink_id; presentation.hyperlink_id = 0; if (!presentation.eql(.{})) try emitSgrTransition(out, presentation); if (hyperlink_id != 0) { if (self.hyperlinkUrl(hyperlink_id)) |url| { try emitHyperlinkOpen( out, self.active_hyperlink_params, url, ); } } }
pub fn writePresentationSteady(self: Grid, out: *std.Io.Writer) !void { var presentation = self.current_style; const hyperlink_id = presentation.hyperlink_id; presentation.hyperlink_id = 0; if (hyperlink_id != 0) { try emitHyperlinkTransition(out, self, hyperlink_id, 0); } if (!presentation.eql(.{})) try out.writeAll("\x1b[0m"); if (self.pending_wrap) try out.writeByte('\r'); }
pub fn copyPresentationStateFrom(self: *Grid, source: Grid) !void { const params = if (source.active_hyperlink_params.len > 0) try self.alloc.dupe(u8, source.active_hyperlink_params) else &.{}; self.replaceActiveHyperlinkParams(params); self.current_style = source.current_style; }
/// Preserve the normal terminal buffer saved by DECSET ?1049 when /// cloning the currently active alternate buffer for a frame commit. pub fn copySavedNormalScreenFrom(self: *Grid, source: Grid) !void { std.debug.assert(self.saved_normal_screen == null); const saved = source.saved_normal_screen orelse return; self.saved_normal_screen = try saved.dupe(self.alloc); }
/// Write one control byte or Unicode display unit. Returns the /// number of source bytes consumed. fn writeByte(self: *Grid, bytes: []const u8, start: usize) !usize { const b = bytes[start];
switch (b) { '\n' => { self.last_printable_idx = null; self.pending_wrap = false; self.cursor_col = 1; self.advanceRowOrScroll(); return 1; }, '\r' => { self.last_printable_idx = null; self.pending_wrap = false; self.cursor_col = 1; return 1; }, 0x08 => { self.last_printable_idx = null; self.pending_wrap = false; if (self.cursor_col > 1) self.cursor_col -= 1; return 1; }, '\t' => { self.last_printable_idx = null; self.pending_wrap = false; self.moveTabsForward(1); return 1; }, 0x07 => { self.last_printable_idx = null; return 1; }, else => { if (b < 0x20) { self.last_printable_idx = null; return 1; } }, }
const unit = display_width.displayUnitAt(bytes, start); const decoded = decodeUtf8(bytes, start); const codepoint = decoded.codepoint; const consumed = unit.byte_len; const width: u8 = @intCast(unit.cell_width);
if (width == 0) { if (self.last_printable_idx) |idx| { try self.appendCombiningSuffix(idx, bytes[start .. start + consumed]); } return consumed; } self.last_printable_idx = null;
// Apply any deferred wrap before writing. if (self.pending_wrap and self.autowrap) { self.cursor_col = 1; self.advanceRowOrScroll(); } self.pending_wrap = false;
// Wide char that doesn't fit: wrap early (autowrap) or clamp. if (self.cursor_col + width - 1 > self.cols) { if (self.autowrap) { self.cursor_col = 1; self.advanceRowOrScroll(); } else if (self.cols >= width) { self.cursor_col = self.cols - width + 1; } else { return consumed; } }
if (self.cursor_row >= 1 and self.cursor_row <= self.rows and self.cursor_col >= 1 and self.cursor_col <= self.cols) { if (self.insert_mode) self.insertCells(width); self.markTouchedRow(self.cursor_row); const idx = self.cellIndex(self.cursor_row, self.cursor_col); self.clearWideGlyphAt(self.cursor_row, self.cursor_col); if (width == 2) self.clearWideGlyphAt(self.cursor_row, self.cursor_col + 1); self.cells[idx] = .{ .codepoint = codepoint, .width = width, .style = self.current_style, }; if (decoded.len < consumed) { try self.appendCombiningSuffix(idx, bytes[start + decoded.len .. start + consumed]); } self.last_printable_idx = idx; if (width == 2 and self.cursor_col < self.cols) { self.cells[idx + 1] = .{ .codepoint = 0, .width = 0, .style = self.current_style, }; } }
if (self.cursor_col + width <= self.cols) { self.cursor_col += width; } else { // Cursor ends at the last column. Defer wrap until the // next printable write (lazy-wrap). self.cursor_col = self.cols; if (self.autowrap) self.pending_wrap = true; }
return consumed; }
fn advanceRowOrScroll(self: *Grid) void { if (self.cursor_row == self.scroll_bottom) { self.scrollUp(self.scroll_top, self.scroll_bottom, 1); return; } if (self.cursor_row < self.rows) { self.cursor_row += 1; } }
fn reverseIndex(self: *Grid) void { if (self.cursor_row == self.scroll_top) { self.scrollDown(self.scroll_top, self.scroll_bottom, 1); } else if (self.cursor_row > 1) { self.cursor_row -= 1; } }
fn scrollUp(self: *Grid, top: u16, bottom: u16, requested: u16) void { if (top == 0 or bottom < top or bottom > self.rows) return; const count = @min(requested, bottom - top + 1); if (count == 0) return; if (self.feed_stats) |stats| { stats.scrolled = true; stats.scroll_rows +|= count; stats.touchRow(bottom); } if (top == 1 and bottom == self.rows and count == 1) { self.row_origin += 1; if (self.row_origin == self.rows) self.row_origin = 0; const last_base = self.rowBase(self.rows); @memset(self.cells[last_base .. last_base + self.cols], self.blankCell()); return; } var row = top; while (row + count <= bottom) : (row += 1) { const destination = self.rowBase(row); const source = self.rowBase(row + count); @memcpy( self.cells[destination .. destination + self.cols], self.cells[source .. source + self.cols], ); } while (row <= bottom) : (row += 1) { const base = self.rowBase(row); @memset(self.cells[base .. base + self.cols], self.blankCell()); } }
fn scrollDown(self: *Grid, top: u16, bottom: u16, requested: u16) void { if (top == 0 or bottom < top or bottom > self.rows) return; const count = @min(requested, bottom - top + 1); if (count == 0) return; self.markTouchedRows(top, bottom); var row = bottom; while (row >= top + count) : (row -= 1) { const destination = self.rowBase(row); const source = self.rowBase(row - count); @memcpy( self.cells[destination .. destination + self.cols], self.cells[source .. source + self.cols], ); } var clear_row = top; while (clear_row < top + count) : (clear_row += 1) { const base = self.rowBase(clear_row); @memset(self.cells[base .. base + self.cols], self.blankCell()); } }
fn param(self: Grid, index: usize, default: u16) u16 { if (index >= self.csi_param_count) return default; const v = self.csi_params[index]; if (v == 0 and !self.csi_has_digit and index + 1 == self.csi_param_count) return default; return if (v == 0) default else v; }
fn paramRaw(self: Grid, index: usize, default: u16) u16 { if (index >= self.csi_param_count) return default; return self.csi_params[index]; }
fn dispatchCsi(self: *Grid, final: u8) !void { switch (final) { 'H', 'f' => self.positionCursor( self.param(0, 1), self.param(1, 1), ), 'A' => { self.cursor_row = clampSub( self.cursor_row, self.param(0, 1), self.cursorTop(), ); self.pending_wrap = false; }, 'B' => { self.cursor_row = clamp( self.cursor_row +| self.param(0, 1), self.cursorTop(), self.cursorBottom(), ); self.pending_wrap = false; }, 'C' => { self.cursor_col = clamp(self.cursor_col +| self.param(0, 1), 1, self.cols); self.pending_wrap = false; }, 'D' => { self.cursor_col = clampSub(self.cursor_col, self.param(0, 1), 1); self.pending_wrap = false; }, 'E' => { self.cursor_row = clamp( self.cursor_row +| self.param(0, 1), self.cursorTop(), self.cursorBottom(), ); self.cursor_col = 1; self.pending_wrap = false; }, 'F' => { self.cursor_row = clampSub( self.cursor_row, self.param(0, 1), self.cursorTop(), ); self.cursor_col = 1; self.pending_wrap = false; }, 'G', '`' => { self.cursor_col = clamp(self.param(0, 1), 1, self.cols); self.pending_wrap = false; }, 'd' => { self.cursor_row = clamp(self.param(0, 1), 1, self.rows); self.pending_wrap = false; }, 'a' => { self.cursor_col = clamp(self.cursor_col +| self.param(0, 1), 1, self.cols); self.pending_wrap = false; }, 'e' => { self.cursor_row = clamp( self.cursor_row +| self.param(0, 1), self.cursorTop(), self.cursorBottom(), ); self.pending_wrap = false; }, 'J' => try self.eraseDisplay(self.paramRaw(0, 0)), 'K' => try self.eraseLine(self.paramRaw(0, 0)), '@' => self.insertCells(self.param(0, 1)), 'P' => self.deleteCells(self.param(0, 1)), 'X' => self.eraseCells(self.param(0, 1)), 'L' => self.insertLines(self.param(0, 1)), 'M' => self.deleteLines(self.param(0, 1)), 'S' => self.scrollUp(self.scroll_top, self.scroll_bottom, self.param(0, 1)), 'T' => self.scrollDown(self.scroll_top, self.scroll_bottom, self.param(0, 1)), 'I' => self.moveTabsForward(self.param(0, 1)), 'Z' => self.moveTabsBackward(self.param(0, 1)), 'g' => self.clearTabStops(self.paramRaw(0, 0)), 'm' => if (self.csi_private == 0) try self.applySgr(), 'h', 'l' => try self.decSetReset(final == 'h'), 'n' => try self.deviceStatusReport(), 'c' => try self.deviceAttributes(), 'r' => self.setScrollRegion(), 's' => self.saveCursor(), 'u' => { if (self.csi_private == 0) { self.restoreCursor(); } else { self.keyboard_protocol = self.csi_private != '<' and self.paramRaw(0, 0) != 0; } }, 't' => try self.windowQuery(), 'q' => if (self.csi_intermediate_count == 1 and self.csi_intermediates[0] == ' ') { self.setCursorStyle(self.paramRaw(0, 0)); }, else => {}, } }
fn cursorTop(self: Grid) u16 { return if (self.origin_mode) self.scroll_top else 1; }
fn cursorBottom(self: Grid) u16 { return if (self.origin_mode) self.scroll_bottom else self.rows; }
fn positionCursor(self: *Grid, row: u16, col: u16) void { const top = self.cursorTop(); const bottom = self.cursorBottom(); const absolute_row = if (self.origin_mode) top +| (row -| 1) else row; self.cursor_row = clamp(absolute_row, top, bottom); self.cursor_col = clamp(col, 1, self.cols); self.pending_wrap = false; }
fn saveCursor(self: *Grid) void { self.saved_cursor = .{ .row = self.cursor_row, .col = self.cursor_col, .pending_wrap = self.pending_wrap, .style = self.current_style, .origin_mode = self.origin_mode, }; }
fn restoreCursor(self: *Grid) void { const saved = self.saved_cursor orelse return; self.cursor_row = clamp(saved.row, 1, self.rows); self.cursor_col = clamp(saved.col, 1, self.cols); self.pending_wrap = saved.pending_wrap; self.current_style = saved.style; self.origin_mode = saved.origin_mode; }
fn setScrollRegion(self: *Grid) void { if (self.csi_private != 0) return; const top = clamp(self.param(0, 1), 1, self.rows); const bottom = clamp(self.param(1, self.rows), 1, self.rows); if (top >= bottom) return; self.scroll_top = top; self.scroll_bottom = bottom; self.positionCursor(1, 1); }
fn insertCells(self: *Grid, requested: u16) void { const count = @min(requested, self.cols - self.cursor_col + 1); if (count == 0) return; self.markTouchedRow(self.cursor_row); const base = self.rowBase(self.cursor_row); const start = base + self.cursor_col - 1; const end = base + self.cols; std.mem.copyBackwards( Cell, self.cells[start + count .. end], self.cells[start .. end - count], ); @memset(self.cells[start .. start + count], self.blankCell()); repairWideCells(self.cells[base .. base + self.cols], self.cols, 1); self.pending_wrap = false; self.last_printable_idx = null; }
fn deleteCells(self: *Grid, requested: u16) void { const count = @min(requested, self.cols - self.cursor_col + 1); if (count == 0) return; self.markTouchedRow(self.cursor_row); const base = self.rowBase(self.cursor_row); const start = base + self.cursor_col - 1; const end = base + self.cols; std.mem.copyForwards( Cell, self.cells[start .. end - count], self.cells[start + count .. end], ); @memset(self.cells[end - count .. end], self.blankCell()); repairWideCells(self.cells[base .. base + self.cols], self.cols, 1); self.pending_wrap = false; self.last_printable_idx = null; }
fn eraseCells(self: *Grid, requested: u16) void { const count = @min(requested, self.cols - self.cursor_col + 1); if (count == 0) return; const start = (@as(usize, self.cursor_row) - 1) * self.cols + self.cursor_col - 1; self.eraseRange(start, start + count); self.markTouchedRow(self.cursor_row); self.pending_wrap = false; self.last_printable_idx = null; }
fn insertLines(self: *Grid, requested: u16) void { if (self.cursor_row < self.scroll_top or self.cursor_row > self.scroll_bottom) return; self.scrollDown(self.cursor_row, self.scroll_bottom, requested); self.pending_wrap = false; self.last_printable_idx = null; }
fn deleteLines(self: *Grid, requested: u16) void { if (self.cursor_row < self.scroll_top or self.cursor_row > self.scroll_bottom) return; self.scrollUp(self.cursor_row, self.scroll_bottom, requested); self.pending_wrap = false; self.last_printable_idx = null; }
fn moveTabsForward(self: *Grid, requested: u16) void { var remaining = requested; while (remaining != 0) : (remaining -= 1) { var column = self.cursor_col +| 1; while (column < self.cols and !self.tab_stops[column - 1]) : (column += 1) {} self.cursor_col = @min(column, self.cols); } self.pending_wrap = false; }
fn moveTabsBackward(self: *Grid, requested: u16) void { var remaining = requested; while (remaining != 0) : (remaining -= 1) { if (self.cursor_col <= 1) break; var column = self.cursor_col - 1; while (column > 1 and !self.tab_stops[column - 1]) : (column -= 1) {} self.cursor_col = column; } self.pending_wrap = false; }
fn clearTabStops(self: *Grid, mode: u16) void { switch (mode) { 0 => self.tab_stops[self.cursor_col - 1] = false, 3 => @memset(self.tab_stops, false), else => {}, } }
fn deviceStatusReport(self: *Grid) !void { const report = self.paramRaw(0, 0); if (report == 5 and self.csi_private == 0) { try self.appendReply("\x1b[0n"); return; } if (report != 6) return; const row = if (self.origin_mode) self.cursor_row - self.scroll_top + 1 else self.cursor_row; var buffer: [64]u8 = undefined; const reply = if (self.csi_private == '?') try std.fmt.bufPrint(&buffer, "\x1b[?{d};{d}R", .{ row, self.cursor_col }) else try std.fmt.bufPrint(&buffer, "\x1b[{d};{d}R", .{ row, self.cursor_col }); try self.appendReply(reply); }
fn deviceAttributes(self: *Grid) !void { if (self.csi_private == '>') { try self.appendReply("\x1b[>0;0;0c"); } else if (self.csi_private == 0) { try self.appendReply("\x1b[?1;2c"); } }
fn windowQuery(self: *Grid) !void { var buffer: [64]u8 = undefined; const reply = switch (self.paramRaw(0, 0)) { 14 => "\x1b[4;0;0t", 16 => "\x1b[6;0;0t", 18 => try std.fmt.bufPrint( &buffer, "\x1b[8;{d};{d}t", .{ self.rows, self.cols }, ), 19 => try std.fmt.bufPrint( &buffer, "\x1b[9;{d};{d}t", .{ self.rows, self.cols }, ), else => return, }; try self.appendReply(reply); }
fn setCursorStyle(self: *Grid, value: u16) void { switch (value) { 0, 1 => { self.cursor_shape = .block; self.cursor_blinking = true; }, 2 => { self.cursor_shape = .block; self.cursor_blinking = false; }, 3 => { self.cursor_shape = .underline; self.cursor_blinking = true; }, 4 => { self.cursor_shape = .underline; self.cursor_blinking = false; }, 5 => { self.cursor_shape = .bar; self.cursor_blinking = true; }, 6 => { self.cursor_shape = .bar; self.cursor_blinking = false; }, else => {}, } }
/// Produce a blank cell in the current erase-style — space /// glyph with default fg/flags but the cursor's active bg. /// Real terminals extend the current bg into erased cells; fx's /// user-message card relies on that behaviour to draw the bar /// without having to pad with literal spaces. fn blankCell(self: Grid) Cell { return .{ .codepoint = ' ', .width = 1, .style = .{ .bg = self.current_style.bg }, }; }
fn clearWideGlyphAt(self: *Grid, row: u16, col: u16) void { if (row == 0 or row > self.rows or col == 0 or col > self.cols) return; const idx = self.cellIndex(row, col); const blank = self.blankCell(); switch (self.cells[idx].width) { 0 => { if (col > 1 and self.cells[idx - 1].width == 2) { self.cells[idx - 1] = blank; } self.cells[idx] = blank; }, 2 => { self.cells[idx] = blank; if (col < self.cols and self.cells[idx + 1].width == 0) { self.cells[idx + 1] = blank; } }, else => {}, } }
fn eraseRange(self: *Grid, start: usize, end: usize) void { std.debug.assert(start <= end and end <= self.cells.len); var expanded_start = start; var expanded_end = end; const cols: usize = self.cols; if (expanded_start < expanded_end and expanded_start % cols != 0 and self.cells[self.physicalIndexForLogicalOffset(expanded_start)].width == 0) { expanded_start -= 1; } if (expanded_end > expanded_start and expanded_end < self.cells.len and expanded_end % cols != 0 and self.cells[self.physicalIndexForLogicalOffset(expanded_end - 1)].width == 2) { expanded_end += 1; } const blank = self.blankCell(); var logical = expanded_start; while (logical < expanded_end) { const col = logical % cols; const chunk_len = @min(cols - col, expanded_end - logical); const physical = self.physicalIndexForLogicalOffset(logical); @memset(self.cells[physical .. physical + chunk_len], blank); logical += chunk_len; } }
fn eraseDisplay(self: *Grid, mode: u16) !void { const total: usize = @as(usize, self.rows) * @as(usize, self.cols); switch (mode) { 0 => { self.markTouchedRows(self.cursor_row, self.rows); const start = (@as(usize, self.cursor_row) - 1) * @as(usize, self.cols) + (@as(usize, self.cursor_col) - 1); self.eraseRange(start, total); }, 1 => { self.markTouchedRows(1, self.cursor_row); const end = (@as(usize, self.cursor_row) - 1) * @as(usize, self.cols) + @as(usize, self.cursor_col); self.eraseRange(0, end); }, 2 => { self.markTouchedRows(1, self.rows); self.eraseRange(0, total); }, 3 => { // ED 3 clears scrollback only; this emulator has no scrollback model. }, else => {}, } }
fn eraseLine(self: *Grid, mode: u16) !void { const row_base = (@as(usize, self.cursor_row) - 1) * @as(usize, self.cols); switch (mode) { 0 => { self.markTouchedRow(self.cursor_row); const start = row_base + (@as(usize, self.cursor_col) - 1); self.eraseRange(start, row_base + @as(usize, self.cols)); }, 1 => { self.markTouchedRow(self.cursor_row); const end = row_base + @as(usize, self.cursor_col); self.eraseRange(row_base, end); }, 2 => { self.markTouchedRow(self.cursor_row); self.eraseRange(row_base, row_base + @as(usize, self.cols)); }, else => {}, } }
fn markTouchedRow(self: *Grid, row: u16) void { if (self.feed_stats) |stats| stats.touchRow(row); }
fn markTouchedRows(self: *Grid, top: u16, bottom: u16) void { if (top == 0 or bottom == 0 or bottom < top) return; if (self.feed_stats) |stats| stats.touchRow(bottom); }
/// Applies supported SGR attributes and palette, indexed, or truecolor values. /// Unknown codes are ignored; an empty parameter list performs a full reset. fn applySgr(self: *Grid) !void { if (self.csi_param_count == 0) { self.resetSgrPresentation(); return; } var i: usize = 0; while (i < self.csi_param_count) { const p = self.csi_params[i]; switch (p) { 0 => self.resetSgrPresentation(), 1 => self.current_style.flags.bold = true, 2 => self.current_style.flags.dim = true, 3 => self.current_style.flags.italic = true, 4 => self.current_style.flags.underline = true, 7 => self.current_style.flags.reverse = true, 9 => self.current_style.flags.strike = true, 22 => { self.current_style.flags.bold = false; self.current_style.flags.dim = false; }, 23 => self.current_style.flags.italic = false, 24 => self.current_style.flags.underline = false, 27 => self.current_style.flags.reverse = false, 29 => self.current_style.flags.strike = false, 30...37 => self.current_style.fg = .{ .indexed = @intCast(p - 30) }, 38 => if (self.extendedColorAt(i)) |ext| { self.current_style.fg = ext.color; i += ext.consumed; continue; }, 39 => self.current_style.fg = .default, 40...47 => self.current_style.bg = .{ .indexed = @intCast(p - 40) }, 48 => if (self.extendedColorAt(i)) |ext| { self.current_style.bg = ext.color; i += ext.consumed; continue; }, 49 => self.current_style.bg = .default, 90...97 => self.current_style.fg = .{ .indexed = @intCast(p - 90 + 8) }, 100...107 => self.current_style.bg = .{ .indexed = @intCast(p - 100 + 8) }, else => {}, } i += 1; } }
fn resetSgrPresentation(self: *Grid) void { const hyperlink_id = self.current_style.hyperlink_id; self.current_style = .{ .hyperlink_id = hyperlink_id }; }
const ExtendedColor = struct { color: Color, consumed: usize };
/// Parse the extended-color params following SGR 38/48: `;5;N` /// (indexed) or `;2;R;G;B` (truecolor), starting at param `i`. /// Returns null on a malformed spec so the caller consumes only /// the introducer. fn extendedColorAt(self: Grid, i: usize) ?ExtendedColor { if (i + 1 >= self.csi_param_count) return null; switch (self.csi_params[i + 1]) { 5 => { if (i + 2 >= self.csi_param_count) return null; const n = self.csi_params[i + 2]; return .{ .color = .{ .indexed = @intCast(@min(n, 255)) }, .consumed = 3 }; }, 2 => { if (i + 4 >= self.csi_param_count) return null; return .{ .color = .{ .rgb = .{ .r = @intCast(@min(self.csi_params[i + 2], 255)), .g = @intCast(@min(self.csi_params[i + 3], 255)), .b = @intCast(@min(self.csi_params[i + 4], 255)), } }, .consumed = 5 }; }, else => return null, } }
fn decSetReset(self: *Grid, set: bool) !void { if (self.csi_private == 0) { var standard_index: usize = 0; while (standard_index < self.csi_param_count) : (standard_index += 1) { if (self.csi_params[standard_index] == 4) self.insert_mode = set; } return; } if (self.csi_private != '?') return; var i: usize = 0; while (i < self.csi_param_count) : (i += 1) { const p = self.csi_params[i]; switch (p) { 1 => self.application_cursor_keys = set, 6 => { self.origin_mode = set; self.positionCursor(1, 1); }, 7 => self.autowrap = set, 25 => self.cursor_visible = set, 47, 1047, 1049 => if (set) try self.enterAlternateScreen() else self.leaveAlternateScreen(), 1000 => self.setMouseMode(0, set), 1002 => self.setMouseMode(1, set), 1003 => self.setMouseMode(2, set), 1005 => self.setMouseMode(3, set), 1006 => self.setMouseMode(4, set), 1015 => self.setMouseMode(5, set), 1004 => self.focus_tracking = set, 2004 => self.bracketed_paste = set, 2026 => self.sync_active = set, else => {}, } } }
fn setMouseMode(self: *Grid, bit: u4, set: bool) void { const mask = @as(u16, 1) << bit; if (set) { self.mouse_modes |= mask; } else { self.mouse_modes &= ~mask; } }
fn resetTerminal(self: *Grid) void { if (self.saved_normal_screen != null) self.leaveAlternateScreen(); @memset(self.cells, .{}); self.row_origin = 0; self.cursor_row = 1; self.cursor_col = 1; self.autowrap = true; self.pending_wrap = false; self.cursor_visible = true; self.cursor_shape = .block; self.cursor_blinking = true; self.scroll_top = 1; self.scroll_bottom = self.rows; self.origin_mode = false; self.insert_mode = false; self.bracketed_paste = false; self.mouse_modes = 0; self.focus_tracking = false; self.application_cursor_keys = false; self.application_keypad = false; self.keyboard_protocol = false; self.sync_active = false; self.sync_buffer.clearRetainingCapacity(); self.current_style = .{}; self.replaceActiveHyperlinkParams(&.{}); for (self.hyperlink_pool.items) |url| self.alloc.free(url); self.hyperlink_pool.clearRetainingCapacity(); self.hyperlink_pool_bytes = 0; for (self.combining_suffix_pool.items) |suffix| self.alloc.free(suffix); self.combining_suffix_pool.clearRetainingCapacity(); self.combining_pool_bytes = 0; self.saved_cursor = null; self.last_printable_idx = null; self.utf8_len = 0; self.utf8_expected = 0; initializeTabStops(self.tab_stops); self.cancelControlSequence(); }
fn enterAlternateScreen(self: *Grid) !void { if (self.saved_normal_screen != null) return;
const alternate_cells = try self.alloc.alloc( Cell, @as(usize, self.cols) * @as(usize, self.rows), ); @memset(alternate_cells, .{});
self.saved_normal_screen = SavedScreen.capture(self.*); self.cells = alternate_cells; self.row_origin = 0; self.cursor_row = 1; self.cursor_col = 1; self.autowrap = true; self.pending_wrap = false; self.cursor_shape = .block; self.cursor_blinking = true; self.scroll_top = 1; self.scroll_bottom = self.rows; self.origin_mode = false; self.insert_mode = false; self.current_style = .{}; self.active_hyperlink_params = &.{}; self.last_printable_idx = null; self.saved_cursor = null; }
fn leaveAlternateScreen(self: *Grid) void { const saved = self.saved_normal_screen orelse return; self.alloc.free(self.cells); if (self.active_hyperlink_params.len > 0) { self.alloc.free(self.active_hyperlink_params); } saved.restoreTo(self); self.saved_normal_screen = null; }
/// Return a copy of the cell at 1-indexed (row, col), or null if /// out of bounds. Used by tests asserting on style (fg/bg/flags), /// since `rowText` deliberately discards attributes. pub fn cellAt(self: Grid, row: u16, col: u16) ?Cell { if (row < 1 or row > self.rows) return null; if (col < 1 or col > self.cols) return null; return self.cells[self.cellIndex(row, col)]; }
/// Return an allocator-owned immutable projection. The caller releases it /// with `OwnedRenderSnapshot.deinit` using the same allocator. pub fn renderSnapshot( self: Grid, alloc: Allocator, ) !contracts.OwnedRenderSnapshot { var text_bytes: usize = 0; var row: u16 = 1; while (row <= self.rows) : (row += 1) { const base = self.rowBase(row); var col: u16 = 0; while (col < self.cols) : (col += 1) { const cell = self.cells[base + col]; if (cell.width == 0 or isBlankCell(cell)) continue; var buffer: [4]u8 = undefined; const encoded_len = std.unicode.utf8Encode(cell.codepoint, &buffer) catch return error.InvalidRenderCell; const suffix_len = if (self.combiningSuffix(cell.combining_suffix_id)) |suffix| suffix.len else if (cell.combining_suffix_id == 0) 0 else return error.InvalidRenderCell; const cell_bytes = std.math.add(usize, encoded_len, suffix_len) catch return error.RenderSnapshotTooLarge; if (cell_bytes > contracts.max_cell_text_bytes) { return error.RenderSnapshotTooLarge; } text_bytes = std.math.add(usize, text_bytes, cell_bytes) catch return error.RenderSnapshotTooLarge; } } const structural_bytes = std.math.mul( usize, self.cells.len, @sizeOf(contracts.RenderCell), ) catch return error.RenderSnapshotTooLarge; if (structural_bytes > contracts.max_render_snapshot_bytes or text_bytes > contracts.max_render_snapshot_bytes - structural_bytes) { return error.RenderSnapshotTooLarge; }
const cells = try alloc.alloc(contracts.RenderCell, self.cells.len); errdefer alloc.free(cells); const text_storage = try alloc.alloc(u8, text_bytes); errdefer alloc.free(text_storage);
var text_offset: usize = 0; var output_index: usize = 0; row = 1; while (row <= self.rows) : (row += 1) { const base = self.rowBase(row); var col: u16 = 0; while (col < self.cols) : (col += 1) { const cell = self.cells[base + col]; const kind: contracts.CellKind = switch (cell.width) { 0 => .continuation, 2 => .wide, else => if (isBlankCell(cell)) .blank else .single, }; var text: []const u8 = ""; if (kind == .single or kind == .wide) { var buffer: [4]u8 = undefined; const encoded_len = std.unicode.utf8Encode(cell.codepoint, &buffer) catch return error.InvalidRenderCell; const start = text_offset; @memcpy(text_storage[text_offset .. text_offset + encoded_len], buffer[0..encoded_len]); text_offset += encoded_len; if (self.combiningSuffix(cell.combining_suffix_id)) |suffix| { @memcpy(text_storage[text_offset .. text_offset + suffix.len], suffix); text_offset += suffix.len; } text = text_storage[start..text_offset]; } cells[output_index] = .{ .kind = kind, .text = text, .style = renderStyle(cell.style), }; output_index += 1; } }
var owned = contracts.OwnedRenderSnapshot{ .dimensions = .{ .rows = self.rows, .columns = self.cols }, .cursor = .{ .row = self.cursor_row - 1, .column = self.cursor_col - 1, .visible = self.cursor_visible, .shape = self.cursor_shape, .blinking = self.cursor_blinking, }, .modes = .{ .alternate_screen = self.saved_normal_screen != null, .origin = self.origin_mode, .autowrap = self.autowrap, .insert = self.insert_mode, .bracketed_paste = self.bracketed_paste, .mouse_tracking = self.mouse_modes != 0, .focus_tracking = self.focus_tracking, .application_cursor_keys = self.application_cursor_keys, .application_keypad = self.application_keypad, .keyboard_protocol = self.keyboard_protocol, .synchronized_updates = self.sync_active, }, .cells = cells, .text_storage = text_storage, }; errdefer owned.deinit(alloc); try owned.view().validate(); return owned; }
/// Serialize all bounded terminal state. The durable store owns cursor /// anchoring and wraps these opaque bytes in its checkpoint envelope. pub fn checkpointPayload(self: Grid, alloc: Allocator) ![]u8 { try self.validateCheckpointState(); var encoder = CheckpointEncoder.init(alloc); errdefer encoder.deinit(); try encoder.bytes(checkpoint_magic); try encoder.int(u16, checkpoint_schema_revision); try encodeGridState(&encoder, self); return encoder.finish(); }
/// Restore an engine payload without performing any I/O or producing /// reply effects. Callers replay later journal bytes observationally. pub fn restoreCheckpoint(alloc: Allocator, payload: []const u8) !Grid { if (payload.len > contracts.max_checkpoint_payload_bytes) { return failGrid(error.CheckpointTooLarge); } var decoder = CheckpointDecoder.init(payload); const magic = try decoder.fixed(checkpoint_magic.len); if (!std.mem.eql(u8, magic, checkpoint_magic)) { return failGrid(error.InvalidEngineCheckpoint); } if (try decoder.int(u16) != checkpoint_schema_revision) { return failGrid(error.UnsupportedEngineRevision); } var grid = try decodeGridState(alloc, &decoder); errdefer grid.deinit(); if (!decoder.finished()) return failGrid(error.InvalidEngineCheckpoint); try grid.validateCheckpointState(); return grid; }
fn validateCheckpointState(self: Grid) !void { if (self.rows == 0 or self.cols == 0 or self.cells.len != @as(usize, self.rows) * @as(usize, self.cols) or self.row_origin >= self.rows or self.cursor_row == 0 or self.cursor_row > self.rows or self.cursor_col == 0 or self.cursor_col > self.cols or self.scroll_top == 0 or self.scroll_top > self.scroll_bottom or self.scroll_bottom > self.rows or self.tab_stops.len != self.cols or self.csi_param_count > max_csi_params or self.csi_intermediate_count > max_csi_intermediates or self.osc_buffer.items.len > max_string_bytes or self.dcs_buffer.items.len > max_string_bytes or self.sync_buffer.items.len > max_sync_bytes or self.utf8_len > self.utf8_expected or self.utf8_expected > self.utf8_buffer.len or self.hyperlink_pool.items.len > max_pool_entries or self.combining_suffix_pool.items.len > max_pool_entries or self.active_hyperlink_params.len > max_string_bytes) { return error.InvalidEngineCheckpoint; } if ((self.state == .csi and self.csi_param_count >= max_csi_params) or (self.state != .osc and self.osc_saw_esc) or (self.state != .dcs and self.dcs_saw_esc) or self.mouse_modes & ~@as(u16, 0x3f) != 0) { return error.InvalidEngineCheckpoint; } try validateUtf8Continuation(self); try validatePool(self.hyperlink_pool.items, max_hyperlink_pool_bytes); try validatePool(self.combining_suffix_pool.items, max_combining_pool_bytes); try validateCells( self.cells, self.cols, self.rows, self.hyperlink_pool.items.len, self.combining_suffix_pool.items.len, ); try validateStyle( self.current_style, self.hyperlink_pool.items.len, ); if (self.last_printable_idx) |index| { if (index >= self.cells.len or self.cells[index].width == 0) { return error.InvalidEngineCheckpoint; } } if (self.saved_cursor) |saved| try validateSavedCursor(saved, self); if (self.saved_normal_screen) |saved| { if (saved.rows != self.rows or saved.cols != self.cols or saved.cells.len != self.cells.len or saved.row_origin >= saved.rows or saved.cursor_row == 0 or saved.cursor_row > saved.rows or saved.cursor_col == 0 or saved.cursor_col > saved.cols or saved.scroll_top == 0 or saved.scroll_top > saved.scroll_bottom or saved.scroll_bottom > saved.rows or saved.active_hyperlink_params.len > max_string_bytes) { return error.InvalidEngineCheckpoint; } try validateCells( saved.cells, saved.cols, saved.rows, self.hyperlink_pool.items.len, self.combining_suffix_pool.items.len, ); try validateStyle(saved.current_style, self.hyperlink_pool.items.len); if (saved.last_printable_idx) |index| { if (index >= saved.cells.len or saved.cells[index].width == 0) { return error.InvalidEngineCheckpoint; } } if (saved.saved_cursor) |cursor| { try validateSavedCursorForDimensions( cursor, saved.rows, saved.cols, self.hyperlink_pool.items.len, ); } } }
/// Append the visible text for a single row (ANSI attributes are /// dropped). Trailing blanks are preserved so positional asserts /// are stable. pub fn rowText(self: Grid, row: u16, out: *std.ArrayList(u8)) !void { if (row == 0 or row > self.rows) return; const base = self.rowBase(row); var col: u16 = 0; var buf: [4]u8 = undefined; while (col < self.cols) : (col += 1) { const cell = self.cells[base + col]; if (cell.width == 0) continue; const cp = if (cell.codepoint == 0) ' ' else cell.codepoint; const n = std.unicode.utf8Encode(cp, &buf) catch 1; try out.appendSlice(self.alloc, buf[0..n]); if (self.combiningSuffix(cell.combining_suffix_id)) |suffix| { try out.appendSlice(self.alloc, suffix); } } }
/// Append the visible text for a single row, trimmed of trailing /// spaces. Useful when asserting "row 5 equals 'foo'". pub fn rowTextTrimmed(self: Grid, row: u16, out: *std.ArrayList(u8)) !void { var tmp: std.ArrayList(u8) = .empty; defer tmp.deinit(self.alloc); try self.rowText(row, &tmp); const trimmed = std.mem.trimEnd(u8, tmp.items, " "); try out.appendSlice(self.alloc, trimmed); }
/// Append one row with its SGR attributes, excluding OSC hyperlinks and /// trailing default cells. pub fn rowStyledTextTrimmed(self: Grid, row: u16, out: *std.Io.Writer) !void { if (row == 0 or row > self.rows) return; var last_col: u16 = 0; var col: u16 = 1; while (col <= self.cols) : (col += 1) { const cell = self.cellAt(row, col).?; if (cell.width == 0) continue; var style = cell.style; style.hyperlink_id = 0; const suffix = self.combiningSuffix(cell.combining_suffix_id) orelse &.{}; if (cell.codepoint != ' ' or suffix.len > 0 or !style.eql(.{})) last_col = col; }
var active_style: Style = .{}; col = 1; while (col <= last_col) : (col += 1) { const cell = self.cellAt(row, col).?; if (cell.width == 0) continue; var style = cell.style; style.hyperlink_id = 0; if (!active_style.eql(style)) { try emitSgrTransition(out, style); active_style = style; } try emitCodepoint(out, cell.codepoint); if (self.combiningSuffix(cell.combining_suffix_id)) |suffix| { try out.writeAll(suffix); } } if (!active_style.eql(.{})) try out.writeAll("\x1b[0m"); }
/// Re-emit the occupied margin glyph without advancing, restoring lazy wrap /// after cursor addressing. The caller enables autowrap and resumes source /// presentation after this sequence; no terminal state is mutated here. pub fn writePendingWrapRestore(self: Grid, row: u16, out: *std.Io.Writer) !void { var col = self.cols; var cell = self.cellAt(row, col) orelse return error.InvalidTranscriptTransition; if (cell.width == 0 and col > 1) { col -= 1; cell = self.cellAt(row, col).?; } if (cell.width == 0 or cell.width - 1 != self.cols - col) { return error.InvalidTranscriptTransition; } try out.print("\x1b[{d};{d}H", .{ row, col }); try emitSgrTransition(out, cell.style); try emitHyperlinkTransition(out, self, 0, cell.style.hyperlink_id); try emitCodepoint(out, cell.codepoint); if (self.combiningSuffix(cell.combining_suffix_id)) |suffix| try out.writeAll(suffix); if (cell.style.hyperlink_id != 0) try emitHyperlinkTransition(out, self, cell.style.hyperlink_id, 0); if (!cell.style.eql(.{})) try out.writeAll("\x1b[0m"); }
test "append pending wrap restore accepts maximum terminal width" { const alloc = std.testing.allocator; var source = try Grid.init(alloc, std.math.maxInt(u16), 1); defer source.deinit(); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try source.writePendingWrapRestore(1, &out.writer); try std.testing.expect(std.mem.startsWith(u8, out.written(), "\x1b[1;65535H")); try std.testing.expectEqual(@as(u8, ' '), out.written()[out.written().len - 1]); }
test "append pending wrap restore preserves margin display cells and presentation" { const alloc = std.testing.allocator; const cases = [_][]const u8{ "12345678", "123456界", "1234567e\u{301}", "123456\x1b[31m\x1b]8;;https://example.com\x1b\\界", }; for (cases) |seed| { var source = try Grid.init(alloc, 8, 2); defer source.deinit(); try source.feed(seed); var physical = try source.clone(alloc); defer physical.deinit(); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try source.writePendingWrapRestore(1, &out.writer); try physical.feed(out.written()); try std.testing.expect(physical.pending_wrap); for (1..9) |col| { try std.testing.expect(cellsEqual(source, source.cellAt(1, @intCast(col)).?, physical, physical.cellAt(1, @intCast(col)).?)); } try std.testing.expect(physical.current_style.eql(.{})); try physical.feed("X"); try std.testing.expectEqual(@as(u21, 'X'), physical.cellAt(2, 1).?.codepoint); try std.testing.expect(physical.cellAt(2, 1).?.style.eql(.{})); } }
/// Produce a deep copy of the cell grid. Cursor and parser state /// are reset to initial values — the clone is intended as a /// frame-buffer snapshot, not a resume point for the emulator. pub fn clone(self: Grid, alloc: Allocator) !Grid { const cells = try alloc.alloc(Cell, self.cells.len); errdefer alloc.free(cells); @memcpy(cells, self.cells);
var pool: std.ArrayList([]u8) = .empty; errdefer { for (pool.items) |url| alloc.free(url); pool.deinit(alloc); } try pool.ensureTotalCapacity(alloc, self.hyperlink_pool.items.len); for (self.hyperlink_pool.items) |url| { const dup = try alloc.dupe(u8, url); pool.appendAssumeCapacity(dup); }
var suffix_pool: std.ArrayList([]u8) = .empty; errdefer { for (suffix_pool.items) |suffix| alloc.free(suffix); suffix_pool.deinit(alloc); } try suffix_pool.ensureTotalCapacity(alloc, self.combining_suffix_pool.items.len); for (self.combining_suffix_pool.items) |suffix| { const dup = try alloc.dupe(u8, suffix); suffix_pool.appendAssumeCapacity(dup); }
const tab_stops = try alloc.dupe(bool, self.tab_stops); errdefer alloc.free(tab_stops);
return .{ .alloc = alloc, .rows = self.rows, .cols = self.cols, .cells = cells, .row_origin = self.row_origin, .scroll_top = 1, .scroll_bottom = self.rows, .tab_stops = tab_stops, .hyperlink_pool = pool, .hyperlink_pool_bytes = self.hyperlink_pool_bytes, .combining_suffix_pool = suffix_pool, .combining_pool_bytes = self.combining_pool_bytes, }; }
/// Emits the ANSI diff between equal-sized grids. Wide-glyph continuation /// cells affect change detection but are not emitted independently. pub fn diffTo(prev: Grid, next: Grid, out: *std.Io.Writer) !void { return diffBand(prev, next, 1, next.rows, out); }
/// Diffs only the inclusive row band, preserving terminal content outside it. /// Empty bands emit nothing; callers that require a final cursor position /// must emit their own CUP sequence. pub fn diffBand( prev: Grid, next: Grid, top_row: u16, bottom_row: u16, out: *std.Io.Writer, ) !void { std.debug.assert(prev.rows == next.rows); std.debug.assert(prev.cols == next.cols); if (bottom_row < top_row) return; const top = @max(top_row, 1); const bot = @min(bottom_row, next.rows);
var cur_style = prev.current_style; var cur_hyperlink = prev.current_style.hyperlink_id; var r: u16 = top; while (r <= bot) : (r += 1) { const prev_base = prev.rowBase(r); const next_base = next.rowBase(r); var first: u16 = 0; var last: u16 = 0; var c: u16 = 0; while (c < next.cols) : (c += 1) { const p = prev.cells[prev_base + c]; const n = next.cells[next_base + c]; if (!cellsEqual(prev, p, next, n)) { if (first == 0) first = c + 1; last = c + 1; } } if (first == 0) continue;
// Cursor-addressed rows must reopen OSC 8 links because tmux ends // an active hyperlink at an automatic terminal wrap. if (cur_hyperlink != 0) { try out.writeAll("\x1b]8;;\x1b\\"); cur_hyperlink = 0; } try out.print("\x1b[{d};{d}H", .{ r, first }); // Repainting one wide glyph across another can leave both cells // blank on real terminals unless the old span is erased first. if (hasShiftedWideCellOverlap(prev, next, r, first, last)) { try out.print("\x1b[{d}X", .{last - first + 1}); }
var col: u16 = first; while (col <= last) { const cell = next.cells[next_base + col - 1]; if (cell.width == 0) { col += 1; continue; } if (!cell.style.eql(cur_style)) { try emitSgrTransition(out, cell.style); cur_style = cell.style; } if (cell.style.hyperlink_id != cur_hyperlink) { try emitHyperlinkTransition(out, next, cur_hyperlink, cell.style.hyperlink_id); cur_hyperlink = cell.style.hyperlink_id; } if (cell.codepoint == '│') try out.print("\x1b[{d}G", .{col}); const suffix = next.combiningSuffix(cell.combining_suffix_id); const guard_margin_suffix = !prev.autowrap and cell.width == 2 and col + 1 == next.cols and suffix != null; // With DECAWM disabled, terminals can drop suffix codepoints // from a display unit whose base ends at the right margin. if (guard_margin_suffix) try out.writeAll("\x1b[?7h"); try emitCodepoint(out, cell.codepoint); if (suffix) |bytes| { try out.writeAll(bytes); } if (guard_margin_suffix) try out.writeAll("\x1b[?7l"); col += cell.width; } }
if (!cur_style.eql(.{})) { try out.writeAll("\x1b[0m"); } if (cur_hyperlink != 0) { try out.writeAll("\x1b]8;;\x1b\\"); } }
/// Dump the full grid as `| row |\n` lines for golden assertions. pub fn snapshot(self: Grid, out: *std.ArrayList(u8)) !void { var r: u16 = 1; while (r <= self.rows) : (r += 1) { try out.append(self.alloc, '|'); try self.rowText(r, out); try out.append(self.alloc, '|'); try out.append(self.alloc, '\n'); } }};
const CheckpointEncoder = struct { alloc: Allocator, output: std.ArrayList(u8) = .empty,
fn init(alloc: Allocator) CheckpointEncoder { return .{ .alloc = alloc }; }
fn deinit(self: *CheckpointEncoder) void { self.output.deinit(self.alloc); }
fn reserve(self: *CheckpointEncoder, additional: usize) !void { if (additional > contracts.max_checkpoint_payload_bytes -| self.output.items.len) { return error.CheckpointTooLarge; } try self.output.ensureUnusedCapacity(self.alloc, additional); }
fn bytes(self: *CheckpointEncoder, value: []const u8) !void { try self.reserve(value.len); self.output.appendSliceAssumeCapacity(value); }
fn sizedBytes(self: *CheckpointEncoder, value: []const u8) !void { const length = std.math.cast(u32, value.len) orelse return error.CheckpointTooLarge; try self.int(u32, length); try self.bytes(value); }
fn int(self: *CheckpointEncoder, comptime T: type, value: T) !void { var buffer: [@sizeOf(T)]u8 = undefined; std.mem.writeInt(T, &buffer, value, .little); try self.bytes(&buffer); }
fn boolean(self: *CheckpointEncoder, value: bool) !void { try self.int(u8, @intFromBool(value)); }
fn finish(self: *CheckpointEncoder) ![]u8 { return self.output.toOwnedSlice(self.alloc); }};
const CheckpointDecoder = struct { input: []const u8, offset: usize = 0,
fn init(input: []const u8) CheckpointDecoder { return .{ .input = input }; }
fn fixed(self: *CheckpointDecoder, length: usize) ![]const u8 { if (length > self.input.len -| self.offset) { return error.InvalidEngineCheckpoint; } const start = self.offset; self.offset += length; return self.input[start..self.offset]; }
fn sizedBytes(self: *CheckpointDecoder, maximum: usize) ![]const u8 { const length = try self.int(u32); if (length > maximum) return error.InvalidEngineCheckpoint; return self.fixed(length); }
fn ownedBytes( self: *CheckpointDecoder, alloc: Allocator, maximum: usize, ) ![]u8 { const value = try self.sizedBytes(maximum); if (value.len == 0) return &.{}; return alloc.dupe(u8, value); }
fn int(self: *CheckpointDecoder, comptime T: type) !T { const value = try self.fixed(@sizeOf(T)); return std.mem.readInt(T, value[0..@sizeOf(T)], .little); }
fn boolean(self: *CheckpointDecoder) !bool { return switch (try self.int(u8)) { 0 => false, 1 => true, else => error.InvalidEngineCheckpoint, }; }
fn finished(self: CheckpointDecoder) bool { return self.offset == self.input.len; }};
fn encodeGridState(encoder: *CheckpointEncoder, grid: Grid) !void { try encoder.int(u16, grid.rows); try encoder.int(u16, grid.cols); try encodeCells(encoder, grid.cells); try encoder.int(u16, grid.row_origin); try encoder.int(u16, grid.cursor_row); try encoder.int(u16, grid.cursor_col); try encoder.boolean(grid.autowrap); try encoder.boolean(grid.pending_wrap); try encoder.boolean(grid.cursor_visible); try encoder.int(u8, @intFromEnum(grid.cursor_shape)); try encoder.boolean(grid.cursor_blinking); try encoder.int(u16, grid.scroll_top); try encoder.int(u16, grid.scroll_bottom); try encoder.boolean(grid.origin_mode); try encoder.boolean(grid.insert_mode); try encoder.boolean(grid.bracketed_paste); try encoder.int(u16, grid.mouse_modes); try encoder.boolean(grid.focus_tracking); try encoder.boolean(grid.application_cursor_keys); try encoder.boolean(grid.application_keypad); try encoder.boolean(grid.keyboard_protocol); for (grid.tab_stops) |stop| try encoder.boolean(stop); try encoder.boolean(grid.sync_active); try encoder.boolean(grid.defer_sync_updates); try encoder.sizedBytes(grid.sync_buffer.items); try encodeStyle(encoder, grid.current_style); try encoder.int(u8, @intFromEnum(grid.state)); for (grid.csi_params) |param| try encoder.int(u16, param); try encoder.int(u8, grid.csi_param_count); try encoder.boolean(grid.csi_has_digit); try encoder.int(u8, grid.csi_private); try encoder.bytes(&grid.csi_intermediates); try encoder.int(u8, grid.csi_intermediate_count); try encoder.boolean(grid.osc_saw_esc); try encoder.sizedBytes(grid.osc_buffer.items); try encoder.boolean(grid.dcs_saw_esc); try encoder.sizedBytes(grid.dcs_buffer.items); try encoder.bytes(&grid.utf8_buffer); try encoder.int(u8, grid.utf8_len); try encoder.int(u8, grid.utf8_expected); try encodePool(encoder, grid.hyperlink_pool.items); try encodePool(encoder, grid.combining_suffix_pool.items); try encoder.sizedBytes(grid.active_hyperlink_params); try encoder.boolean(grid.saved_normal_screen != null); if (grid.saved_normal_screen) |saved| try encodeSavedScreen(encoder, saved); try encodeOptionalSavedCursor(encoder, grid.saved_cursor); try encodeOptionalIndex(encoder, grid.last_printable_idx);}
fn encodeCells(encoder: *CheckpointEncoder, cells: []const Cell) !void { const count = std.math.cast(u32, cells.len) orelse return error.CheckpointTooLarge; try encoder.int(u32, count); for (cells) |cell| { try encoder.int(u32, cell.codepoint); try encoder.int(u8, cell.width); try encoder.int(u32, cell.combining_suffix_id); try encodeStyle(encoder, cell.style); }}
fn encodeStyle(encoder: *CheckpointEncoder, style: Style) !void { try encodeColor(encoder, style.fg); try encodeColor(encoder, style.bg); try encoder.int(u8, @bitCast(style.flags)); try encoder.int(u32, style.hyperlink_id);}
fn encodeColor(encoder: *CheckpointEncoder, color: Color) !void { switch (color) { .default => try encoder.int(u8, 0), .indexed => |index| { try encoder.int(u8, 1); try encoder.int(u8, index); }, .rgb => |rgb| { try encoder.int(u8, 2); try encoder.int(u8, rgb.r); try encoder.int(u8, rgb.g); try encoder.int(u8, rgb.b); }, }}
fn encodePool(encoder: *CheckpointEncoder, pool: []const []u8) !void { const count = std.math.cast(u32, pool.len) orelse return error.CheckpointTooLarge; try encoder.int(u32, count); for (pool) |value| try encoder.sizedBytes(value);}
fn encodeSavedScreen( encoder: *CheckpointEncoder, saved: SavedScreen,) !void { try encoder.int(u16, saved.rows); try encoder.int(u16, saved.cols); try encodeCells(encoder, saved.cells); try encoder.int(u16, saved.row_origin); try encoder.int(u16, saved.cursor_row); try encoder.int(u16, saved.cursor_col); try encoder.boolean(saved.autowrap); try encoder.boolean(saved.pending_wrap); try encoder.boolean(saved.cursor_visible); try encoder.int(u8, @intFromEnum(saved.cursor_shape)); try encoder.boolean(saved.cursor_blinking); try encodeStyle(encoder, saved.current_style); try encoder.sizedBytes(saved.active_hyperlink_params); try encodeOptionalIndex(encoder, saved.last_printable_idx); try encoder.int(u16, saved.scroll_top); try encoder.int(u16, saved.scroll_bottom); try encoder.boolean(saved.origin_mode); try encoder.boolean(saved.insert_mode); try encodeOptionalSavedCursor(encoder, saved.saved_cursor);}
fn encodeOptionalSavedCursor( encoder: *CheckpointEncoder, cursor: ?SavedCursor,) !void { try encoder.boolean(cursor != null); if (cursor) |saved| { try encoder.int(u16, saved.row); try encoder.int(u16, saved.col); try encoder.boolean(saved.pending_wrap); try encodeStyle(encoder, saved.style); try encoder.boolean(saved.origin_mode); }}
fn encodeOptionalIndex( encoder: *CheckpointEncoder, index: ?usize,) !void { try encoder.boolean(index != null); if (index) |value| { const encoded = std.math.cast(u64, value) orelse return error.CheckpointTooLarge; try encoder.int(u64, encoded); }}
fn decodeGridState( alloc: Allocator, decoder: *CheckpointDecoder,) !Grid { const rows = try decoder.int(u16); const cols = try decoder.int(u16); var grid = try Grid.init(alloc, cols, rows); errdefer grid.deinit(); try decodeCells(decoder, grid.cells); grid.row_origin = try decoder.int(u16); grid.cursor_row = try decoder.int(u16); grid.cursor_col = try decoder.int(u16); grid.autowrap = try decoder.boolean(); grid.pending_wrap = try decoder.boolean(); grid.cursor_visible = try decoder.boolean(); grid.cursor_shape = try decodeCursorShape(decoder); grid.cursor_blinking = try decoder.boolean(); grid.scroll_top = try decoder.int(u16); grid.scroll_bottom = try decoder.int(u16); grid.origin_mode = try decoder.boolean(); grid.insert_mode = try decoder.boolean(); grid.bracketed_paste = try decoder.boolean(); grid.mouse_modes = try decoder.int(u16); grid.focus_tracking = try decoder.boolean(); grid.application_cursor_keys = try decoder.boolean(); grid.application_keypad = try decoder.boolean(); grid.keyboard_protocol = try decoder.boolean(); for (grid.tab_stops) |*stop| stop.* = try decoder.boolean(); grid.sync_active = try decoder.boolean(); grid.defer_sync_updates = try decoder.boolean(); grid.sync_buffer = .fromOwnedSlice(try decoder.ownedBytes( alloc, max_sync_bytes, )); grid.current_style = try decodeStyle(decoder); grid.state = switch (try decoder.int(u8)) { 0 => .normal, 1 => .escape, 2 => .csi, 3 => .osc, 4 => .dcs, else => return error.InvalidEngineCheckpoint, }; for (&grid.csi_params) |*param| param.* = try decoder.int(u16); grid.csi_param_count = try decoder.int(u8); grid.csi_has_digit = try decoder.boolean(); grid.csi_private = try decoder.int(u8); @memcpy( &grid.csi_intermediates, try decoder.fixed(grid.csi_intermediates.len), ); grid.csi_intermediate_count = try decoder.int(u8); grid.osc_saw_esc = try decoder.boolean(); grid.osc_buffer = .fromOwnedSlice(try decoder.ownedBytes( alloc, max_string_bytes, )); grid.dcs_saw_esc = try decoder.boolean(); grid.dcs_buffer = .fromOwnedSlice(try decoder.ownedBytes( alloc, max_string_bytes, )); @memcpy(&grid.utf8_buffer, try decoder.fixed(grid.utf8_buffer.len)); grid.utf8_len = try decoder.int(u8); grid.utf8_expected = try decoder.int(u8); try decodePool( decoder, alloc, &grid.hyperlink_pool, &grid.hyperlink_pool_bytes, max_hyperlink_pool_bytes, ); try decodePool( decoder, alloc, &grid.combining_suffix_pool, &grid.combining_pool_bytes, max_combining_pool_bytes, ); grid.active_hyperlink_params = try decoder.ownedBytes( alloc, max_string_bytes, ); if (try decoder.boolean()) { grid.saved_normal_screen = try decodeSavedScreen( decoder, alloc, grid.cells.len, ); } grid.saved_cursor = try decodeOptionalSavedCursor(decoder); grid.last_printable_idx = try decodeOptionalIndex(decoder); return grid;}
fn decodeCells(decoder: *CheckpointDecoder, cells: []Cell) !void { const count = try decoder.int(u32); if (count != cells.len) return error.InvalidEngineCheckpoint; for (cells) |*cell| { const codepoint = try decoder.int(u32); if (codepoint > std.math.maxInt(u21)) { return error.InvalidEngineCheckpoint; } cell.* = .{ .codepoint = @intCast(codepoint), .width = try decoder.int(u8), .combining_suffix_id = try decoder.int(u32), .style = try decodeStyle(decoder), }; }}
fn decodeStyle(decoder: *CheckpointDecoder) !Style { const fg = try decodeColor(decoder); const bg = try decodeColor(decoder); const flags = try decoder.int(u8); if (flags & 0xc0 != 0) return error.InvalidEngineCheckpoint; return .{ .fg = fg, .bg = bg, .flags = @bitCast(flags), .hyperlink_id = try decoder.int(u32), };}
fn decodeColor(decoder: *CheckpointDecoder) !Color { return switch (try decoder.int(u8)) { 0 => .default, 1 => .{ .indexed = try decoder.int(u8) }, 2 => .{ .rgb = .{ .r = try decoder.int(u8), .g = try decoder.int(u8), .b = try decoder.int(u8), } }, else => error.InvalidEngineCheckpoint, };}
fn decodeCursorShape(decoder: *CheckpointDecoder) !contracts.CursorShape { return switch (try decoder.int(u8)) { 0 => .block, 1 => .underline, 2 => .bar, else => error.InvalidEngineCheckpoint, };}
fn decodePool( decoder: *CheckpointDecoder, alloc: Allocator, pool: *std.ArrayList([]u8), byte_count: *usize, maximum_bytes: usize,) !void { const count = try decoder.int(u32); if (count > max_pool_entries) return error.InvalidEngineCheckpoint; try pool.ensureTotalCapacity(alloc, count); var total: usize = 0; for (0..count) |_| { const value = try decoder.ownedBytes(alloc, max_string_bytes); errdefer if (value.len > 0) alloc.free(value); if (value.len == 0 or value.len > maximum_bytes -| total) { return error.InvalidEngineCheckpoint; } pool.appendAssumeCapacity(value); total += value.len; } byte_count.* = total;}
fn decodeSavedScreen( decoder: *CheckpointDecoder, alloc: Allocator, expected_cell_count: usize,) !SavedScreen { const rows = try decoder.int(u16); const cols = try decoder.int(u16); const count = std.math.mul( usize, @intCast(rows), @intCast(cols), ) catch return error.InvalidEngineCheckpoint; if (count != expected_cell_count) return error.InvalidEngineCheckpoint; const cells = try alloc.alloc(Cell, count); errdefer alloc.free(cells); try decodeCells(decoder, cells); const row_origin = try decoder.int(u16); const cursor_row = try decoder.int(u16); const cursor_col = try decoder.int(u16); const autowrap = try decoder.boolean(); const pending_wrap = try decoder.boolean(); const cursor_visible = try decoder.boolean(); const cursor_shape = try decodeCursorShape(decoder); const cursor_blinking = try decoder.boolean(); const current_style = try decodeStyle(decoder); const active_params = try decoder.sizedBytes(max_string_bytes); const last_printable_idx = try decodeOptionalIndex(decoder); const scroll_top = try decoder.int(u16); const scroll_bottom = try decoder.int(u16); const origin_mode = try decoder.boolean(); const insert_mode = try decoder.boolean(); const saved_cursor = try decodeOptionalSavedCursor(decoder); const owned_params = if (active_params.len == 0) &.{} else try alloc.dupe(u8, active_params); return .{ .rows = rows, .cols = cols, .cells = cells, .row_origin = row_origin, .cursor_row = cursor_row, .cursor_col = cursor_col, .autowrap = autowrap, .pending_wrap = pending_wrap, .cursor_visible = cursor_visible, .cursor_shape = cursor_shape, .cursor_blinking = cursor_blinking, .current_style = current_style, .active_hyperlink_params = owned_params, .last_printable_idx = last_printable_idx, .scroll_top = scroll_top, .scroll_bottom = scroll_bottom, .origin_mode = origin_mode, .insert_mode = insert_mode, .saved_cursor = saved_cursor, };}
fn decodeOptionalSavedCursor( decoder: *CheckpointDecoder,) !?SavedCursor { if (!try decoder.boolean()) return null; return .{ .row = try decoder.int(u16), .col = try decoder.int(u16), .pending_wrap = try decoder.boolean(), .style = try decodeStyle(decoder), .origin_mode = try decoder.boolean(), };}
fn decodeOptionalIndex(decoder: *CheckpointDecoder) !?usize { if (!try decoder.boolean()) return null; return std.math.cast(usize, try decoder.int(u64)) orelse error.InvalidEngineCheckpoint;}
fn validatePool(pool: []const []u8, maximum_bytes: usize) !void { var total: usize = 0; for (pool) |value| { if (value.len == 0 or value.len > max_string_bytes or value.len > maximum_bytes -| total) { return error.InvalidEngineCheckpoint; } total += value.len; }}
fn validateUtf8Continuation(grid: Grid) !void { if (grid.utf8_len == 0) { if (grid.utf8_expected != 0) return error.InvalidEngineCheckpoint; return; } if (grid.state != .normal or grid.utf8_expected < 2) { return error.InvalidEngineCheckpoint; } const expected = std.unicode.utf8ByteSequenceLength(grid.utf8_buffer[0]) catch return error.InvalidEngineCheckpoint; if (expected != grid.utf8_expected) return error.InvalidEngineCheckpoint; var index: usize = 1; while (index < grid.utf8_len) : (index += 1) { if (grid.utf8_buffer[index] & 0xc0 != 0x80) { return error.InvalidEngineCheckpoint; } }}
fn validateCells( cells: []const Cell, cols: u16, rows: u16, hyperlink_count: usize, combining_count: usize,) !void { if (cells.len != @as(usize, cols) * @as(usize, rows)) { return error.InvalidEngineCheckpoint; } var row: u16 = 0; while (row < rows) : (row += 1) { const base = @as(usize, row) * @as(usize, cols); var col: u16 = 0; while (col < cols) : (col += 1) { const cell = cells[base + col]; try validateStyle(cell.style, hyperlink_count); if (cell.combining_suffix_id > combining_count) { return error.InvalidEngineCheckpoint; } if (cell.codepoint != 0) { _ = std.unicode.utf8CodepointSequenceLength(cell.codepoint) catch return error.InvalidEngineCheckpoint; } switch (cell.width) { 0 => { if (col == 0 or cell.codepoint != 0 or cell.combining_suffix_id != 0) { return error.InvalidEngineCheckpoint; } const lead = cells[base + col - 1]; if (lead.width != 2 or !lead.style.eql(cell.style)) { return error.InvalidEngineCheckpoint; } }, 1 => if (cell.codepoint == 0) { return error.InvalidEngineCheckpoint; }, 2 => { if (cell.codepoint == 0 or col + 1 >= cols) { return error.InvalidEngineCheckpoint; } const continuation = cells[base + col + 1]; if (continuation.width != 0 or continuation.codepoint != 0 or continuation.combining_suffix_id != 0 or !continuation.style.eql(cell.style)) { return error.InvalidEngineCheckpoint; } }, else => return error.InvalidEngineCheckpoint, } } }}
fn validateStyle(style: Style, hyperlink_count: usize) !void { if (style.hyperlink_id > hyperlink_count) { return error.InvalidEngineCheckpoint; }}
fn validateSavedCursor(cursor: SavedCursor, grid: Grid) !void { return validateSavedCursorForDimensions( cursor, grid.rows, grid.cols, grid.hyperlink_pool.items.len, );}
fn validateSavedCursorForDimensions( cursor: SavedCursor, rows: u16, cols: u16, hyperlink_count: usize,) !void { if (cursor.row == 0 or cursor.row > rows or cursor.col == 0 or cursor.col > cols) { return error.InvalidEngineCheckpoint; } try validateStyle(cursor.style, hyperlink_count);}
fn initializeTabStops(stops: []bool) void { @memset(stops, false); var index: usize = 8; while (index < stops.len) : (index += 8) stops[index] = true;}
fn appendBounded( list: *std.ArrayList(u8), alloc: Allocator, byte: u8, limit: usize,) !void { if (list.items.len >= limit) return error.ControlStringTooLarge; try list.append(alloc, byte);}
fn isBlankCell(cell: Cell) bool { return cell.width == 1 and cell.codepoint == ' ' and cell.combining_suffix_id == 0;}
fn renderStyle(style: Style) contracts.CellStyle { return .{ .foreground = renderColor(style.fg), .background = renderColor(style.bg), .bold = style.flags.bold, .faint = style.flags.dim, .italic = style.flags.italic, .underline = style.flags.underline, .inverse = style.flags.reverse, .strikethrough = style.flags.strike, };}
fn renderColor(color: Color) contracts.CellColor { return switch (color) { .default => .default, .indexed => |index| .{ .indexed = index }, .rgb => |rgb| .{ .rgb = .{ .red = rgb.r, .green = rgb.g, .blue = rgb.b, } }, };}
fn physicalRowIndex(origin: u16, logical_row: u16, rows: u16) usize { std.debug.assert(rows > 0); std.debug.assert(origin < rows); std.debug.assert(logical_row < rows); const index = @as(usize, origin) + @as(usize, logical_row); return if (index < rows) index else index - rows;}
fn cellsEqual(a_grid: Grid, a: Cell, b_grid: Grid, b: Cell) bool { if (a.codepoint != b.codepoint or a.width != b.width or !a.style.eql(b.style)) { return false; } if (a.combining_suffix_id == 0 or b.combining_suffix_id == 0) { return a.combining_suffix_id == b.combining_suffix_id; } const a_suffix = a_grid.combiningSuffix(a.combining_suffix_id) orelse return false; const b_suffix = b_grid.combiningSuffix(b.combining_suffix_id) orelse return false; return std.mem.eql(u8, a_suffix, b_suffix);}
fn hasShiftedWideCellOverlap( prev: Grid, next: Grid, row: u16, first: u16, last: u16,) bool { const prev_base = prev.rowBase(row); const next_base = next.rowBase(row); var col = first; while (col <= last) : (col += 1) { const prev_lead = wideCellLead(prev.cells, prev_base, col); const next_lead = wideCellLead(next.cells, next_base, col); if (prev_lead != null and next_lead != null and prev_lead.? != next_lead.?) { return true; } } return false;}
fn wideCellLead(cells: []const Cell, row_base: usize, col: u16) ?u16 { const idx = row_base + @as(usize, col - 1); return switch (cells[idx].width) { 2 => col, 0 => if (col > 1 and cells[idx - 1].width == 2) col - 1 else null, else => null, };}
/// Transitions between OSC 8 hyperlinks without closing a valid active link first.fn emitHyperlinkTransition(out: *std.Io.Writer, grid: Grid, prev_id: u32, next_id: u32) !void { // OSC 8 permits opening a new link without explicitly closing the active one. if (next_id == 0) { try out.writeAll("\x1b]8;;\x1b\\"); return; } const url = grid.hyperlinkUrl(next_id) orelse { if (prev_id != 0) try out.writeAll("\x1b]8;;\x1b\\"); return; }; try emitHyperlinkOpen(out, &.{}, url);}
fn emitHyperlinkOpen( out: *std.Io.Writer, params: []const u8, url: []const u8,) !void { try out.writeAll("\x1b]8;"); try out.writeAll(params); try out.writeByte(';'); try out.writeAll(url); try out.writeAll("\x1b\\");}
/// Resets unknown prior style before applying the target state.fn emitSgrTransition(out: *std.Io.Writer, next: Style) !void { try out.writeAll("\x1b[0m"); if (next.flags.bold) try out.writeAll("\x1b[1m"); if (next.flags.dim) try out.writeAll("\x1b[2m"); if (next.flags.italic) try out.writeAll("\x1b[3m"); if (next.flags.underline) try out.writeAll("\x1b[4m"); if (next.flags.reverse) try out.writeAll("\x1b[7m"); if (next.flags.strike) try out.writeAll("\x1b[9m"); try emitColor(out, next.fg, .fg); try emitColor(out, next.bg, .bg);}
const ColorChannel = enum { fg, bg };
fn emitColor(out: *std.Io.Writer, color: Color, channel: ColorChannel) !void { switch (color) { .default => {}, .indexed => |i| { if (i < 8) { const base: u16 = if (channel == .fg) 30 else 40; try out.print("\x1b[{d}m", .{base + @as(u16, i)}); } else if (i < 16) { const base: u16 = if (channel == .fg) 90 else 100; try out.print("\x1b[{d}m", .{base + @as(u16, i) - 8}); } else { const prefix: u16 = if (channel == .fg) 38 else 48; try out.print("\x1b[{d};5;{d}m", .{ prefix, i }); } }, .rgb => |rgb| { const prefix: u16 = if (channel == .fg) 38 else 48; try out.print("\x1b[{d};2;{d};{d};{d}m", .{ prefix, rgb.r, rgb.g, rgb.b }); }, }}
fn emitCodepoint(out: *std.Io.Writer, cp: u21) !void { var buf: [4]u8 = undefined; const cp_resolved: u21 = if (cp == 0) ' ' else cp; const n = std.unicode.utf8Encode(cp_resolved, &buf) catch blk: { buf[0] = ' '; break :blk 1; }; try out.writeAll(buf[0..n]);}
fn clamp(v: u16, lo: u16, hi: u16) u16 { if (hi < lo) return lo; if (v < lo) return lo; if (v > hi) return hi; return v;}
fn clampSub(v: u16, sub: u16, lo: u16) u16 { if (sub >= v) return lo; const r = v - sub; return if (r < lo) lo else r;}
const DecodedRune = struct { codepoint: u21, len: usize };
fn decodeUtf8(bytes: []const u8, start: usize) DecodedRune { const b = bytes[start]; const seq_len = std.unicode.utf8ByteSequenceLength(b) catch 1; if (seq_len <= 1 or start + seq_len > bytes.len) { return .{ .codepoint = if (b < 0x80) b else 0xfffd, .len = 1, }; } const slice = bytes[start .. start + seq_len]; const cp = std.unicode.utf8Decode(slice) catch return .{ .codepoint = 0xfffd, .len = 1 }; return .{ .codepoint = cp, .len = seq_len };}
fn repairWideCells(cells: []Cell, cols: u16, rows: u16) void { var row: u16 = 0; while (row < rows) : (row += 1) { const row_base = @as(usize, row) * @as(usize, cols); var col: u16 = 0; while (col < cols) : (col += 1) { const idx = row_base + @as(usize, col); switch (cells[idx].width) { 0 => { if (col == 0 or cells[idx].codepoint != 0 or cells[idx].combining_suffix_id != 0 or cells[idx - 1].width != 2 or !cells[idx].style.eql(cells[idx - 1].style)) { cells[idx] = .{}; } }, 1 => {}, 2 => { if (col + 1 >= cols or cells[idx + 1].width != 0 or cells[idx + 1].codepoint != 0 or cells[idx + 1].combining_suffix_id != 0 or !cells[idx + 1].style.eql(cells[idx].style)) { cells[idx] = .{}; } }, else => cells[idx] = .{}, } } }}
const testing = std.testing;
fn expectRow(grid: Grid, row: u16, expected: []const u8) !void { var buf: std.ArrayList(u8) = .empty; defer buf.deinit(testing.allocator); try grid.rowTextTrimmed(row, &buf); try testing.expectEqualStrings(expected, buf.items);}
fn expectWideCellInvariant(grid: Grid) !void { var row: u16 = 1; while (row <= grid.rows) : (row += 1) { var col: u16 = 1; while (col <= grid.cols) : (col += 1) { const cell = grid.cellAt(row, col).?; switch (cell.width) { 0 => { try testing.expect(col > 1); const lead = grid.cellAt(row, col - 1).?; try testing.expectEqual(@as(u8, 2), lead.width); try testing.expectEqual(@as(u21, 0), cell.codepoint); try testing.expect(cell.style.eql(lead.style)); }, 1 => {}, 2 => { try testing.expect(col < grid.cols); const continuation = grid.cellAt(row, col + 1).?; try testing.expectEqual(@as(u8, 0), continuation.width); try testing.expectEqual(@as(u21, 0), continuation.codepoint); try testing.expect(continuation.style.eql(cell.style)); }, else => return error.TestUnexpectedResult, } } }}
test "plain writes land on the grid" { var g = try Grid.init(testing.allocator, 10, 3); defer g.deinit(); try g.feed("hello"); try expectRow(g, 1, "hello"); try testing.expectEqual(@as(u16, 6), g.cursor_col); try testing.expectEqual(@as(u16, 1), g.cursor_row);}
test "CUP moves the cursor" { var g = try Grid.init(testing.allocator, 10, 4); defer g.deinit(); try g.feed("\x1b[2;3Hab"); try expectRow(g, 2, " ab"); try testing.expectEqual(@as(u16, 5), g.cursor_col); try testing.expectEqual(@as(u16, 2), g.cursor_row);}
test "LF advances to the next row at column 1" { var g = try Grid.init(testing.allocator, 10, 3); defer g.deinit(); try g.feed("a\nb\nc"); try expectRow(g, 1, "a"); try expectRow(g, 2, "b"); try expectRow(g, 3, "c");}
test "CR returns to column 1 without advancing row" { var g = try Grid.init(testing.allocator, 10, 2); defer g.deinit(); try g.feed("abc\rXY"); try expectRow(g, 1, "XYc");}
test "EL 2K clears the whole line" { var g = try Grid.init(testing.allocator, 10, 2); defer g.deinit(); try g.feed("hello world"); try g.feed("\x1b[1;1H\x1b[2K"); try expectRow(g, 1, "");}
test "EL K clears cursor to end of line" { var g = try Grid.init(testing.allocator, 10, 2); defer g.deinit(); try g.feed("hello"); try g.feed("\x1b[1;3H\x1b[K"); try expectRow(g, 1, "he");}
test "EL 0 expands a continuation boundary to the complete wide glyph" { var g = try Grid.init(testing.allocator, 4, 1); defer g.deinit(); try g.feed("ab界"); try g.feed("\x1b[1;4H\x1b[K"); try expectWideCellInvariant(g); try expectRow(g, 1, "ab");}
test "EL 1 expands a lead boundary to the complete wide glyph" { var g = try Grid.init(testing.allocator, 4, 1); defer g.deinit(); try g.feed("界xy"); try g.feed("\x1b[1;1H\x1b[1K"); try expectWideCellInvariant(g); try expectRow(g, 1, " xy");}
test "ED J clears cursor to end of display" { var g = try Grid.init(testing.allocator, 5, 3); defer g.deinit(); try g.feed("\x1b[1;1Haaaaa\x1b[2;1Hbbbbb\x1b[3;1Hccccc"); try g.feed("\x1b[2;1H\x1b[J"); try expectRow(g, 1, "aaaaa"); try expectRow(g, 2, ""); try expectRow(g, 3, "");}
test "ED 0 expands a continuation boundary to the complete wide glyph" { var g = try Grid.init(testing.allocator, 4, 2); defer g.deinit(); try g.feed("ab界"); try g.feed("\x1b[1;4H\x1b[J"); try expectWideCellInvariant(g); try expectRow(g, 1, "ab"); try expectRow(g, 2, "");}
test "ED 1 expands a lead boundary to the complete wide glyph" { var g = try Grid.init(testing.allocator, 4, 2); defer g.deinit(); try g.feed("\x1b[2;1H界xy"); try g.feed("\x1b[2;1H\x1b[1J"); try expectWideCellInvariant(g); try expectRow(g, 1, ""); try expectRow(g, 2, " xy");}
test "ED 2J clears the entire display" { var g = try Grid.init(testing.allocator, 4, 2); defer g.deinit(); try g.feed("\x1b[1;1Hab\x1b[2;1Hcd"); try g.feed("\x1b[2J"); try expectRow(g, 1, ""); try expectRow(g, 2, "");}
test "autowrap wraps past cols; ?7l disables wrap" { var g = try Grid.init(testing.allocator, 4, 3); defer g.deinit(); try g.feed("abcdef"); try expectRow(g, 1, "abcd"); try expectRow(g, 2, "ef");
try g.feed("\x1b[3;1H\x1b[?7lXXXXXYY"); try expectRow(g, 3, "XXXY");}
test "horizontal tab clears pending wrap and uses the next absolute stop" { var margin = try Grid.init(testing.allocator, 8, 2); defer margin.deinit(); var stats: FeedStats = .{}; try margin.feedWithStats("12345678\tX", &stats); try expectRow(margin, 1, "1234567X"); try expectRow(margin, 2, ""); try testing.expectEqual(@as(u16, 1), margin.cursor_row); try testing.expectEqual(@as(u16, 8), margin.cursor_col); try testing.expect(margin.pending_wrap); try testing.expectEqual(@as(u16, 0), stats.scroll_rows);
var ordinary = try Grid.init(testing.allocator, 16, 1); defer ordinary.deinit(); try ordinary.feed("a\tb"); try expectRow(ordinary, 1, "a b"); try testing.expectEqual(@as(u16, 10), ordinary.cursor_col);}
test "combining marks preserve base-character wrap geometry" { var base = try Grid.init(testing.allocator, 4, 2); defer base.deinit(); var base_stats: FeedStats = .{}; try base.feedWithStats("eeeee", &base_stats);
var decomposed = try Grid.init(testing.allocator, 4, 2); defer decomposed.deinit(); var decomposed_stats: FeedStats = .{}; try decomposed.feedWithStats("e\u{0301}e\u{0301}e\u{0301}e\u{0301}e\u{0301}", &decomposed_stats);
try testing.expectEqual(base.cursor_row, decomposed.cursor_row); try testing.expectEqual(base.cursor_col, decomposed.cursor_col); try testing.expectEqual(base.pending_wrap, decomposed.pending_wrap); try testing.expectEqual(base_stats.max_row_touched, decomposed_stats.max_row_touched); try testing.expectEqual(base_stats.scrolled, decomposed_stats.scrolled); try testing.expectEqual(base_stats.scroll_rows, decomposed_stats.scroll_rows);}
test "combining marks remain attached to the base cell text" { var g = try Grid.init(testing.allocator, 8, 1); defer g.deinit();
try g.feed("e\u{0301}\u{0327}x");
try expectRow(g, 1, "e\u{0301}\u{0327}x"); try testing.expectEqual(@as(u16, 3), g.cursor_col);}
test "repeated combining suffixes reuse grid storage" { var g = try Grid.init(testing.allocator, 8, 1); defer g.deinit();
try g.feed("e\u{0301}e\u{0301}e\u{0301}");
try testing.expectEqual(@as(usize, 1), g.combining_suffix_pool.items.len);}
test "combining marks survive Grid clone" { var source = try Grid.init(testing.allocator, 8, 1); defer source.deinit(); try source.feed("e\u{0301}\u{0327}");
var cloned = try source.clone(testing.allocator); defer cloned.deinit();
try expectRow(cloned, 1, "e\u{0301}\u{0327}");}
test "combining suffix clears with overwritten and erased cells" { var g = try Grid.init(testing.allocator, 8, 1); defer g.deinit();
try g.feed("e\u{0301}"); try g.feed("\x1b[1;1Hx"); try expectRow(g, 1, "x"); try testing.expectEqual(@as(u32, 0), g.cellAt(1, 1).?.combining_suffix_id);
try g.feed("\x1b[1;1He\u{0301}\x1b[2K"); try expectRow(g, 1, ""); try testing.expectEqual(@as(u32, 0), g.cellAt(1, 1).?.combining_suffix_id);}
test "combining marks attach to a wide lead at pending wrap" { var g = try Grid.init(testing.allocator, 3, 1); defer g.deinit();
try g.feed("a界\u{0301}");
try expectRow(g, 1, "a界\u{0301}"); try testing.expect(g.pending_wrap); try testing.expectEqual(@as(u16, 3), g.cursor_col); try testing.expect(g.cellAt(1, 2).?.combining_suffix_id != 0); try testing.expectEqual(@as(u32, 0), g.cellAt(1, 3).?.combining_suffix_id);}
test "Unicode display units preserve bytes and terminal geometry" { const Case = struct { text: []const u8, width: u8, }; const cases = [_]Case{ .{ .text = "\u{2600}\u{FE0E}", .width = 1 }, .{ .text = "\u{231A}\u{FE0E}", .width = 2 }, .{ .text = "\u{2600}\u{FE0F}", .width = 2 }, .{ .text = "\u{1F44D}\u{1F3FD}", .width = 2 }, .{ .text = "\u{1F1FA}\u{1F1F8}", .width = 2 }, .{ .text = "#\u{FE0F}\u{20E3}", .width = 2 }, .{ .text = "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", .width = 2 }, .{ .text = "\u{1F469}\u{200D}\u{1F4BB}", .width = 2 }, };
for (cases) |case| { var grid = try Grid.init(testing.allocator, 16, 1); defer grid.deinit(); try grid.feed("A"); try grid.feed(case.text); try grid.feed("B");
var expected: std.ArrayList(u8) = .empty; defer expected.deinit(testing.allocator); try expected.append(testing.allocator, 'A'); try expected.appendSlice(testing.allocator, case.text); try expected.append(testing.allocator, 'B'); try expectRow(grid, 1, expected.items); try testing.expectEqual(@as(u16, case.width) + 3, grid.cursor_col);
const lead = grid.cellAt(1, 2).?; try testing.expectEqual(case.width, lead.width); const first = decodeUtf8(case.text, 0); try testing.expectEqual(first.codepoint, lead.codepoint); try testing.expectEqualStrings( case.text[first.len..], grid.combiningSuffix(lead.combining_suffix_id).?, );
var cloned = try grid.clone(testing.allocator); defer cloned.deinit(); try expectRow(cloned, 1, expected.items); }}
test "Unicode display-unit suffixes clear on overwrite and erase" { const cases = [_][]const u8{ "\u{2600}\u{FE0E}", "\u{1F44D}\u{1F3FD}", "\u{1F469}\u{200D}\u{1F4BB}", };
for (cases) |text| { var grid = try Grid.init(testing.allocator, 8, 1); defer grid.deinit();
try grid.feed(text); try grid.feed("\x1b[1;1HX"); try expectRow(grid, 1, "X"); try testing.expectEqual(@as(u32, 0), grid.cellAt(1, 1).?.combining_suffix_id);
try grid.feed("\x1b[1;1H"); try grid.feed(text); try grid.feed("\x1b[1;1H\x1b[2K"); try expectRow(grid, 1, ""); try testing.expectEqual(@as(u32, 0), grid.cellAt(1, 1).?.combining_suffix_id); }}
test "Unicode display units survive resize while intact and clear when clipped" { const tag_flag = "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}"; var grid = try Grid.init(testing.allocator, 3, 1); defer grid.deinit(); try grid.feed(tag_flag ++ "x");
try grid.resize(6, 2); try expectRow(grid, 1, tag_flag ++ "x"); try grid.resize(3, 1); try expectRow(grid, 1, tag_flag ++ "x"); try grid.resize(1, 1); try expectRow(grid, 1, ""); try testing.expectEqual(@as(u32, 0), grid.cellAt(1, 1).?.combining_suffix_id);}
test "SGR is swallowed, text content is preserved" { var g = try Grid.init(testing.allocator, 8, 1); defer g.deinit(); try g.feed("\x1b[38;5;240m[sys]\x1b[0m x"); try expectRow(g, 1, "[sys] x");}
test "private keyboard mode does not alter SGR presentation" { var g = try Grid.init(testing.allocator, 8, 1); defer g.deinit();
try g.feed("\x1b[>4;2m");
try testing.expect(g.current_style.eql(.{}));}
test "OSC title is discarded without affecting the grid" { var g = try Grid.init(testing.allocator, 6, 1); defer g.deinit(); try g.feed("\x1b]2;my title\x07text"); try expectRow(g, 1, "text");}
test "OSC with ST (ESC backslash) terminator is discarded" { var g = try Grid.init(testing.allocator, 6, 1); defer g.deinit(); try g.feed("\x1b]11;rgb:0000/0000/0000\x1b\\hello"); try expectRow(g, 1, "hello");}
test "DSR 6n is swallowed without side effects" { var g = try Grid.init(testing.allocator, 6, 1); defer g.deinit(); try g.feed("\x1b[6n"); try g.feed("ok"); try expectRow(g, 1, "ok");}
test "resize grows keeping top-left content" { var g = try Grid.init(testing.allocator, 4, 2); defer g.deinit(); try g.feed("abcd\nef"); try g.resize(6, 3); try expectRow(g, 1, "abcd"); try expectRow(g, 2, "ef"); try expectRow(g, 3, ""); try testing.expectEqual(@as(u16, 6), g.cols); try testing.expectEqual(@as(u16, 3), g.rows);}
test "resize shrinks clipping bottom/right without clearing" { var g = try Grid.init(testing.allocator, 6, 3); defer g.deinit(); try g.feed("\x1b[1;1Haaabbb\x1b[2;1Hxxxxxx\x1b[3;1Hyyyyyy"); try g.resize(4, 2); try expectRow(g, 1, "aaab"); try expectRow(g, 2, "xxxx"); try testing.expectEqual(@as(u16, 4), g.cols); try testing.expectEqual(@as(u16, 2), g.rows);}
test "resize narrowing clears a wide glyph clipped at the right edge" { var g = try Grid.init(testing.allocator, 4, 1); defer g.deinit(); try g.feed("ab界"); try g.resize(3, 1); try expectWideCellInvariant(g); try expectRow(g, 1, "ab");}
test "writes clear any complete wide glyph overlapping the destination" { const Case = struct { initial: []const u8, col: u16, replacement: []const u8, }; const cases = [_]Case{ .{ .initial = "界xy", .col = 1, .replacement = "a" }, .{ .initial = "界xy", .col = 2, .replacement = "a" }, .{ .initial = "界zz", .col = 2, .replacement = "界" }, }; for (cases) |case| { var g = try Grid.init(testing.allocator, 4, 1); defer g.deinit(); try g.feed(case.initial); var cursor: [16]u8 = undefined; const move = try std.fmt.bufPrint(&cursor, "\x1b[1;{d}H", .{case.col}); try g.feed(move); try g.feed(case.replacement); try expectWideCellInvariant(g); }}
test "scroll on LF at last row" { var g = try Grid.init(testing.allocator, 4, 2); defer g.deinit(); try g.feed("AAAA\nBBBB"); try g.feed("\nCCCC"); try expectRow(g, 1, "BBBB"); try expectRow(g, 2, "CCCC");}
test "repeated scroll rotations preserve logical rows through clone resize and erase" { var grid = try Grid.init(testing.allocator, 5, 3); defer grid.deinit(); try grid.feed("one\ntwo\nthree\nfour\nfive");
try expectRow(grid, 1, "three"); try expectRow(grid, 2, "four"); try expectRow(grid, 3, "five");
var cloned = try grid.clone(testing.allocator); defer cloned.deinit(); try testing.expect(gridsEqual(grid, cloned));
try cloned.feed("\x1b[2;2H\x1b[K"); try expectRow(cloned, 1, "three"); try expectRow(cloned, 2, "f"); try expectRow(cloned, 3, "five");
try grid.resize(7, 4); try expectRow(grid, 1, "three"); try expectRow(grid, 2, "four"); try expectRow(grid, 3, "five"); try expectRow(grid, 4, ""); try testing.expectEqual(@as(u16, 0), grid.row_origin);}
test "diff compares logical rows across different physical origins" { var previous = try Grid.init(testing.allocator, 5, 3); defer previous.deinit(); try previous.feed("one\ntwo\nthree\nfour");
var target = try Grid.init(testing.allocator, 5, 3); defer target.deinit(); try target.feed("two\nthree\nfour"); try target.feed("\x1b[2;2HX");
var diff: std.Io.Writer.Allocating = .init(testing.allocator); defer diff.deinit(); try Grid.diffTo(previous, target, &diff.writer); try previous.feed(diff.written());
try testing.expect(gridsEqual(previous, target));}
test "alternate screen restores a rotated normal grid" { var grid = try Grid.init(testing.allocator, 5, 3); defer grid.deinit(); try grid.feed("one\ntwo\nthree\nfour");
try grid.feed("\x1b[?1049h"); try grid.feed("alternate"); try grid.feed("\x1b[?1049l");
try expectRow(grid, 1, "two"); try expectRow(grid, 2, "three"); try expectRow(grid, 3, "four");}
test "wide and combining cells survive repeated row rotations" { var grid = try Grid.init(testing.allocator, 6, 2); defer grid.deinit(); try grid.feed("界e\u{0301}\nplain\n界e\u{0301}");
try expectWideCellInvariant(grid); try expectRow(grid, 1, "plain"); try expectRow(grid, 2, "界e\u{0301}");}
test "snapshot renders full grid" { var g = try Grid.init(testing.allocator, 3, 2); defer g.deinit(); try g.feed("\x1b[1;1Hab\x1b[2;1Hcd");
var buf: std.ArrayList(u8) = .empty; defer buf.deinit(testing.allocator); try g.snapshot(&buf); try testing.expectEqualStrings("|ab |\n|cd |\n", buf.items);}
test "sync updates buffer until DECRST" { var g = try Grid.init(testing.allocator, 4, 2); defer g.deinit();
try g.feed("\x1b[?2026h"); try g.feed("ab"); try expectRow(g, 1, "");
try g.feed("\x1b[?2026l"); try expectRow(g, 1, "ab");}
test "alternate screen restores the normal grid after DECRST 1049" { var g = try Grid.init(testing.allocator, 8, 2); defer g.deinit();
try g.feed("normal"); try g.feed("\x1b[?25l\x1b[?1049h"); try testing.expect(!g.cursor_visible); try g.feed("approval"); try expectRow(g, 1, "approval");
try g.feed("\x1b[?25h\x1b[?1049l"); try testing.expect(g.cursor_visible); try expectRow(g, 1, "normal");}
test "alternate screen preserves the resized normal grid after DECRST 1049" { var g = try Grid.init(testing.allocator, 8, 2); defer g.deinit();
try g.feed("normal"); try g.feed("\x1b[?1049h"); try g.feed("approval"); try g.resize(12, 3); try g.feed("\x1b[2;1Hresized approval");
try g.feed("\x1b[?1049l"); try expectRow(g, 1, "normal"); try testing.expectEqual(@as(u16, 12), g.cols); try testing.expectEqual(@as(u16, 3), g.rows);}
test "partial CSI across feed boundaries" { var g = try Grid.init(testing.allocator, 6, 2); defer g.deinit(); try g.feed("\x1b[2;"); try g.feed("3Hx"); try expectRow(g, 2, " x");}
test "CAN and SUB cancel partial CSI and OSC parser state" { var g = try Grid.init(testing.allocator, 12, 2); defer g.deinit();
try g.feed("\x1b[2;"); try g.feed("\x18\x1b[1;1Hcsi"); try expectRow(g, 1, "csi");
try g.feed("\x1b]8;;https://bad.example"); try g.feed("\x1aplain"); try expectRow(g, 1, "csiplain"); try testing.expectEqual(@as(u32, 0), g.current_style.hyperlink_id);}
test "SGR tracks indexed bg and applies it to written cells" { var g = try Grid.init(testing.allocator, 6, 1); defer g.deinit(); try g.feed("\x1b[48;5;236mhi\x1b[0m"); const c1 = g.cellAt(1, 1).?; const c2 = g.cellAt(1, 2).?; const c3 = g.cellAt(1, 3).?; try testing.expect(c1.style.bg.eql(.{ .indexed = 236 })); try testing.expect(c2.style.bg.eql(.{ .indexed = 236 })); try testing.expect(c3.style.bg.eql(.default));}
test "EL with active bg fills cleared cells with that bg" { var g = try Grid.init(testing.allocator, 6, 1); defer g.deinit(); try g.feed("\x1b[48;5;236mhi\x1b[K"); var col: u16 = 1; while (col <= 6) : (col += 1) { const c = g.cellAt(1, col).?; try testing.expect(c.style.bg.eql(.{ .indexed = 236 })); }}
test "SGR 0 resets to default" { var g = try Grid.init(testing.allocator, 4, 1); defer g.deinit(); try g.feed("\x1b[1;4;48;5;236mhi\x1b[0mok"); const hi1 = g.cellAt(1, 1).?; try testing.expect(hi1.style.flags.bold); try testing.expect(hi1.style.flags.underline); try testing.expect(hi1.style.bg.eql(.{ .indexed = 236 })); const ok1 = g.cellAt(1, 3).?; try testing.expect(!ok1.style.flags.bold); try testing.expect(!ok1.style.flags.underline); try testing.expect(ok1.style.bg.eql(.default));}
test "SGR 9 and 29 apply and clear strikethrough" { var g = try Grid.init(testing.allocator, 3, 1); defer g.deinit(); try g.feed("\x1b[9mx\x1b[29my");
try testing.expect(g.cellAt(1, 1).?.style.flags.strike); try testing.expect(!g.cellAt(1, 2).?.style.flags.strike);}
test "presentation boundary resumes and steadies strikethrough" { var source = try Grid.init(testing.allocator, 2, 1); defer source.deinit(); try source.feed("\x1b[9mx");
var resume_writer: std.Io.Writer.Allocating = .init(testing.allocator); defer resume_writer.deinit(); try source.writePresentationResume(&resume_writer.writer); try testing.expectEqualStrings("\x1b[0m\x1b[9m", resume_writer.written());
var resumed = try Grid.init(testing.allocator, 2, 1); defer resumed.deinit(); try resumed.feed(resume_writer.written()); try resumed.feed("y"); try testing.expect(resumed.cellAt(1, 1).?.style.flags.strike);
var steady_writer: std.Io.Writer.Allocating = .init(testing.allocator); defer steady_writer.deinit(); try source.writePresentationSteady(&steady_writer.writer); try testing.expectEqualStrings("\x1b[0m", steady_writer.written());
try resumed.feed(steady_writer.written()); try resumed.feed("z"); try testing.expect(!resumed.cellAt(1, 2).?.style.flags.strike);}
test "presentation resume preserves OSC 8 parameters and close clears them" { var source = try Grid.init(testing.allocator, 4, 1); defer source.deinit(); try source.feed("\x1b]8;id=fx-42;https://example.com\x1b\\x");
var resume_writer: std.Io.Writer.Allocating = .init(testing.allocator); defer resume_writer.deinit(); try source.writePresentationResume(&resume_writer.writer); try testing.expectEqualStrings( "\x1b]8;id=fx-42;https://example.com\x1b\\", resume_writer.written(), );
var steady_writer: std.Io.Writer.Allocating = .init(testing.allocator); defer steady_writer.deinit(); try source.writePresentationSteady(&steady_writer.writer); try testing.expectEqualStrings("\x1b]8;;\x1b\\", steady_writer.written());
try source.feed("\x1b]8;;\x1b\\"); var closed_writer: std.Io.Writer.Allocating = .init(testing.allocator); defer closed_writer.deinit(); try source.writePresentationResume(&closed_writer.writer); try testing.expectEqualStrings("", closed_writer.written());}
test "OSC 8 parameter replacement is atomic on allocation failure" { var failing = testing.FailingAllocator.init(testing.allocator, .{}); const alloc = failing.allocator(); var source = try Grid.init(alloc, 4, 1); defer source.deinit(); try source.feed("\x1b]8;id=fx-old;https://example.com\x1b\\"); try source.osc_buffer.ensureTotalCapacity(alloc, 128);
failing.fail_index = failing.alloc_index; try testing.expectError( error.OutOfMemory, source.feed("\x1b]8;id=fx-new;https://example.com\x1b\\"), ); try testing.expect(source.atControlSequenceBoundary());
var old_resume_buf: [128]u8 = undefined; var old_resume: std.Io.Writer = .fixed(&old_resume_buf); try source.writePresentationResume(&old_resume); try testing.expectEqualStrings( "\x1b]8;id=fx-old;https://example.com\x1b\\", old_resume.buffered(), );
failing.fail_index = std.math.maxInt(usize); try source.feed("\x1b]8;id=fx-new;https://example.com\x1b\\"); try testing.expectEqual(@as(usize, 1), source.hyperlink_pool.items.len);
var new_resume_buf: [128]u8 = undefined; var new_resume: std.Io.Writer = .fixed(&new_resume_buf); try source.writePresentationResume(&new_resume); try testing.expectEqualStrings( "\x1b]8;id=fx-new;https://example.com\x1b\\", new_resume.buffered(), );}
test "SGR reset preserves active OSC 8 hyperlink until explicit close" { var g = try Grid.init(testing.allocator, 4, 1); defer g.deinit(); try g.feed("\x1b]8;;https://example.com\x1b\\\x1b[31mx\x1b[0my\x1b]8;;\x1b\\z");
const x = g.cellAt(1, 1).?; const y = g.cellAt(1, 2).?; const z = g.cellAt(1, 3).?; try testing.expect(x.style.hyperlink_id != 0); try testing.expectEqual(x.style.hyperlink_id, y.style.hyperlink_id); try testing.expectEqual(@as(u32, 0), z.style.hyperlink_id); try testing.expect(y.style.fg.eql(.default));}
test "SGR truecolor 38;2;R;G;B" { var g = try Grid.init(testing.allocator, 2, 1); defer g.deinit(); try g.feed("\x1b[38;2;10;20;30mx"); const c = g.cellAt(1, 1).?; try testing.expect(c.style.fg.eql(.{ .rgb = .{ .r = 10, .g = 20, .b = 30 } }));}
test "empty SGR \\x1b[m resets like \\x1b[0m" { var g = try Grid.init(testing.allocator, 3, 1); defer g.deinit(); try g.feed("\x1b[1;31mx\x1b[my"); const x = g.cellAt(1, 1).?; try testing.expect(x.style.flags.bold); const y = g.cellAt(1, 2).?; try testing.expect(!y.style.flags.bold); try testing.expect(y.style.fg.eql(.default));}
fn gridsEqual(a: Grid, b: Grid) bool { if (a.rows != b.rows or a.cols != b.cols) return false; var row: u16 = 1; while (row <= a.rows) : (row += 1) { var col: u16 = 1; while (col <= a.cols) : (col += 1) { if (!cellsEqual(a, a.cellAt(row, col).?, b, b.cellAt(row, col).?)) { return false; } } } return true;}
/// Verifies that applying the diff between two grids reproduces the target grid.fn assertDiffRoundTrip( cols: u16, rows: u16, initial_bytes: []const u8, mutate_bytes: []const u8,) !void { var prev = try Grid.init(testing.allocator, cols, rows); defer prev.deinit(); try prev.feed(initial_bytes);
var next = try prev.clone(testing.allocator); defer next.deinit(); try next.feed(mutate_bytes);
var diff_buf: std.ArrayList(u8) = .empty; defer diff_buf.deinit(testing.allocator); var writer = std.Io.Writer.Allocating.fromArrayList(testing.allocator, &diff_buf); try Grid.diffTo(prev, next, &writer.writer); diff_buf = writer.toArrayList();
try prev.feed(diff_buf.items); try testing.expect(gridsEqual(prev, next));}
test "diffTo round-trip: single cell change" { try assertDiffRoundTrip(10, 2, "hello\nworld", "\x1b[1;3HX");}
test "diffTo round-trip: multi-row change" { try assertDiffRoundTrip(6, 3, "aaa\nbbb\nccc", "\x1b[1;1HZZZ\x1b[3;2HYY");}
test "diffTo round-trip: bg color added" { try assertDiffRoundTrip(6, 1, "hello ", "\x1b[1;1H\x1b[48;5;236mHI\x1b[0m");}
test "diffTo round-trip: no change yields empty-effect diff" { try assertDiffRoundTrip(4, 2, "abcd\nefgh", "");}
test "diffTo round-trip: full-row replacement" { try assertDiffRoundTrip(8, 2, "oldline1\noldline2", "\x1b[1;1Hnewline1\x1b[2;1Hnewline2");}
test "diffTo round-trip: wide cell replacement" { try assertDiffRoundTrip(6, 1, "abcdef", "\x1b[1;2H\xe7\x95\x8c");}
test "diffTo round-trips exact Unicode display-unit bytes" { const cases = [_][]const u8{ "\u{2600}\u{FE0E}", "\u{2600}\u{FE0F}", "\u{1F44D}\u{1F3FD}", "\u{1F1FA}\u{1F1F8}", "#\u{FE0F}\u{20E3}", "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", "\u{1F469}\u{200D}\u{1F4BB}", }; for (cases) |text| try assertDiffRoundTrip(8, 1, "", text);}
test "Unicode display diff anchors vertical dividers at their grid columns" { var prev = try Grid.init(testing.allocator, 12, 1); defer prev.deinit(); var next = try prev.clone(testing.allocator); defer next.deinit(); try next.feed("│ \u{231A}\u{FE0E} │");
var diff: std.Io.Writer.Allocating = .init(testing.allocator); defer diff.deinit(); try Grid.diffTo(prev, next, &diff.writer); try testing.expect(std.mem.find(u8, diff.written(), "\x1b[10G│") != null);}
test "diffBand emits combining suffix changes" { var prev = try Grid.init(testing.allocator, 4, 1); defer prev.deinit(); try prev.feed("e");
var next = try Grid.init(testing.allocator, 4, 1); defer next.deinit(); try next.feed("e\u{0301}");
var diff: std.Io.Writer.Allocating = .init(testing.allocator); defer diff.deinit(); try Grid.diffBand(prev, next, 1, 1, &diff.writer); try testing.expect(std.mem.find(u8, diff.written(), "\u{0301}") != null);
try prev.feed(diff.written()); try expectRow(prev, 1, "e\u{0301}");}
test "diffBand clears shifted wide-cell overlap before repaint" { var prev = try Grid.init(testing.allocator, 8, 1); defer prev.deinit(); prev.autowrap = false; try prev.feed("\x1b[1;3HA🇺🇸B");
var next = try Grid.init(testing.allocator, 8, 1); defer next.deinit(); next.autowrap = false; try next.feed("\x1b[1;3H🇺🇸B ");
var diff: std.Io.Writer.Allocating = .init(testing.allocator); defer diff.deinit(); try Grid.diffBand(prev, next, 1, 1, &diff.writer); try testing.expect(std.mem.find(u8, diff.written(), "\x1b[1;3H\x1b[4X") != null);
var ascii = try Grid.init(testing.allocator, 8, 1); defer ascii.deinit(); try ascii.feed("\x1b[1;3Htext"); var ascii_next = try Grid.init(testing.allocator, 8, 1); defer ascii_next.deinit(); try ascii_next.feed("\x1b[1;3Hnext");
var ascii_diff: std.Io.Writer.Allocating = .init(testing.allocator); defer ascii_diff.deinit(); try Grid.diffBand(ascii, ascii_next, 1, 1, &ascii_diff.writer); try testing.expect(std.mem.find(u8, ascii_diff.written(), "\x1b[4X") == null);
var stationary = try Grid.init(testing.allocator, 8, 1); defer stationary.deinit(); try stationary.feed("\x1b[1;3H🇺🇸"); var stationary_next = try Grid.init(testing.allocator, 8, 1); defer stationary_next.deinit(); try stationary_next.feed("\x1b[1;3H🇨🇦");
var stationary_diff: std.Io.Writer.Allocating = .init(testing.allocator); defer stationary_diff.deinit(); try Grid.diffBand(stationary, stationary_next, 1, 1, &stationary_diff.writer); try testing.expect(std.mem.find(u8, stationary_diff.written(), "X") == null);}
test "diffBand temporarily enables autowrap for a suffixed wide cell at the margin" { var prev = try Grid.init(testing.allocator, 6, 2); defer prev.deinit(); prev.autowrap = false;
var next = try Grid.init(testing.allocator, 6, 2); defer next.deinit(); next.autowrap = false; try next.feed("\x1b[1;5H👩💻\x1b[2;1HZ");
var diff: std.Io.Writer.Allocating = .init(testing.allocator); defer diff.deinit(); try Grid.diffBand(prev, next, 1, 2, &diff.writer); try testing.expect(std.mem.find(u8, diff.written(), "\x1b[?7h👩💻\x1b[?7l") != null);
var non_margin_prev = try Grid.init(testing.allocator, 6, 1); defer non_margin_prev.deinit(); non_margin_prev.autowrap = false; var non_margin = try Grid.init(testing.allocator, 6, 1); defer non_margin.deinit(); non_margin.autowrap = false; try non_margin.feed("\x1b[1;4H👩💻");
var non_margin_diff: std.Io.Writer.Allocating = .init(testing.allocator); defer non_margin_diff.deinit(); try Grid.diffBand(non_margin_prev, non_margin, 1, 1, &non_margin_diff.writer); try testing.expect(std.mem.find(u8, non_margin_diff.written(), "\x1b[?7h") == null); try testing.expect(std.mem.find(u8, non_margin_diff.written(), "👩💻") != null);}
test "diffTo emits \\x1b[0m at end when last style was non-default" { var prev = try Grid.init(testing.allocator, 4, 1); defer prev.deinit(); var next = try prev.clone(testing.allocator); defer next.deinit(); try next.feed("\x1b[1;31mAB");
var buf: std.ArrayList(u8) = .empty; defer buf.deinit(testing.allocator); var writer = std.Io.Writer.Allocating.fromArrayList(testing.allocator, &buf); try Grid.diffTo(prev, next, &writer.writer); buf = writer.toArrayList(); try testing.expect(std.mem.find(u8, buf.items, "\x1b[0m") != null);}
test "OSC 8 hyperlink round-trips through diffBand" { var prev = try Grid.init(testing.allocator, 16, 1); defer prev.deinit(); var next = try prev.clone(testing.allocator); defer next.deinit(); try next.feed("\x1b]8;;https://x.com/vercel_dev\x1b\\@vercel_dev\x1b]8;;\x1b\\");
var buf: std.ArrayList(u8) = .empty; defer buf.deinit(testing.allocator); var writer = std.Io.Writer.Allocating.fromArrayList(testing.allocator, &buf); try Grid.diffBand(prev, next, 1, 1, &writer.writer); buf = writer.toArrayList();
try testing.expect(std.mem.find(u8, buf.items, "\x1b]8;;https://x.com/vercel_dev\x1b\\") != null); try testing.expect(std.mem.endsWith(u8, buf.items, "\x1b]8;;\x1b\\"));}
test "diffBand reopens an OSC 8 hyperlink for each emitted row" { var prev = try Grid.init(testing.allocator, 4, 2); defer prev.deinit(); var next = try prev.clone(testing.allocator); defer next.deinit(); try next.feed("\x1b]8;;https://example.com\x1b\\abcd\x1b[2;1Hefgh\x1b]8;;\x1b\\");
var buf: std.ArrayList(u8) = .empty; defer buf.deinit(testing.allocator); var writer = std.Io.Writer.Allocating.fromArrayList(testing.allocator, &buf); try Grid.diffBand(prev, next, 1, 2, &writer.writer); buf = writer.toArrayList();
const marker = "\x1b]8;;https://example.com\x1b\\"; var opens: usize = 0; var start: usize = 0; while (std.mem.indexOf(u8, buf.items[start..], marker)) |offset| { opens += 1; start += offset + marker.len; } try testing.expectEqual(@as(usize, 2), opens);}
test "diffBand clears active presentation before plain repaint cells" { var previous = try Grid.init(testing.allocator, 8, 2); defer previous.deinit(); previous.defer_sync_updates = false; try previous.feed("\x1b]8;;https://active.example\x1b\\\x1b[1m");
var target = try Grid.init(testing.allocator, 8, 2); defer target.deinit(); target.defer_sync_updates = false; try target.feed("q");
var diff: std.Io.Writer.Allocating = .init(testing.allocator); defer diff.deinit(); try Grid.diffBand(previous, target, 1, 1, &diff.writer); try previous.feed(diff.written());
const cell = previous.cellAt(1, 1).?; try testing.expectEqual(@as(u21, 'q'), cell.codepoint); try testing.expect(!cell.style.flags.bold); try testing.expectEqual(@as(u32, 0), cell.style.hyperlink_id); try testing.expect(previous.current_style.eql(.{}));}
test "OSC 8 hyperlink survives Grid.clone" { var src = try Grid.init(testing.allocator, 16, 1); defer src.deinit(); try src.feed("\x1b]8;;https://example.com\x1b\\X\x1b]8;;\x1b\\");
var dup = try src.clone(testing.allocator); defer dup.deinit();
const cell = dup.cellAt(1, 1).?; try testing.expect(cell.style.hyperlink_id != 0); const url = dup.hyperlinkUrl(cell.style.hyperlink_id).?; try testing.expectEqualStrings("https://example.com", url);}
test "production editing scroll region origin save restore and modes" { var grid = try Grid.init(testing.allocator, 8, 4); defer grid.deinit();
try grid.feed("abcdefgh\x1b[1;3H\x1b[2@XY\x1b[P\x1b[2X"); try expectRow(grid, 1, "abXY f");
try grid.feed("\x1b[2;4r\x1b[?6h\x1b[1;1HA\nB\nC\nD"); try testing.expectEqual(@as(u16, 2), grid.scroll_top); try testing.expectEqual(@as(u16, 4), grid.scroll_bottom); try testing.expect(grid.origin_mode); try expectRow(grid, 2, "B"); try expectRow(grid, 3, "C"); try expectRow(grid, 4, "D");
try grid.feed("\x1b7\x1b[4;5HZ\x1b8Q"); try testing.expectEqual(@as(u21, 'Q'), grid.cellAt(4, 2).?.codepoint); try grid.feed("\x1b[?7l\x1b[4h\x1b[?2004h\x1b[?1000h\x1b[?1004h\x1b[>1u"); try testing.expect(!grid.autowrap); try testing.expect(grid.insert_mode); try testing.expect(grid.bracketed_paste); try testing.expect(grid.mouse_modes != 0); try testing.expect(grid.focus_tracking); try testing.expect(grid.keyboard_protocol);}
test "tabs alternate screen and fragmented UTF-8 preserve cell invariants" { var grid = try Grid.init(testing.allocator, 12, 3); defer grid.deinit(); try grid.feed("A\tB\x1bH\r\tC"); try testing.expectEqual(@as(u21, 'C'), grid.cellAt(1, 9).?.codepoint); try grid.feed("\x1b[?1049hALT\x1b[?1049l"); try testing.expectEqual(@as(u21, 'A'), grid.cellAt(1, 1).?.codepoint); try grid.feed("\xe7\x95"); try grid.feed("\x8ce"); try grid.feed("\xcc\x81"); try expectWideCellInvariant(grid);}
test "fragmented native queries emit one ordered reply and observational modes emit none" { var native = try Grid.init(testing.allocator, 80, 24); defer native.deinit(); try native.feed("\x1b[3;7H"); var first = try native.feedMode("\x1b[6", .native_live); defer first.deinit(testing.allocator); try testing.expectEqual(@as(usize, 0), first.replies.items.len); var second = try native.feedMode("n\x1b[18t", .native_live); defer second.deinit(testing.allocator); try testing.expectEqual(@as(usize, 2), second.replies.items.len); try testing.expectEqualStrings("\x1b[3;7R", second.replies.items[0].bytes); try testing.expectEqualStrings("\x1b[8;24;80t", second.replies.items[1].bytes);
var tmux = try Grid.init(testing.allocator, 80, 24); defer tmux.deinit(); var tmux_result = try tmux.feedMode("\x1b[6n\x1b[18t", .tmux_live); defer tmux_result.deinit(testing.allocator); try testing.expectEqual(@as(usize, 0), tmux_result.replies.items.len);
var replay = try Grid.init(testing.allocator, 80, 24); defer replay.deinit(); var replay_result = try replay.feedMode("\x1b[6n\x1b[18t", .journal_replay); defer replay_result.deinit(testing.allocator); try testing.expectEqual(@as(usize, 0), replay_result.replies.items.len);}
test "render snapshot is immutable row-major styled state" { var grid = try Grid.init(testing.allocator, 5, 2); defer grid.deinit(); try grid.feed("\x1b[31;44;1mA\xe7\x95\x8c\x1b[?25l\x1b[5 q"); var snapshot = try grid.renderSnapshot(testing.allocator); defer snapshot.deinit(testing.allocator); try snapshot.view().validate(); try testing.expectEqual(contracts.CellKind.single, snapshot.cells[0].kind); try testing.expect(snapshot.cells[0].style.bold); try testing.expectEqual(contracts.CellKind.wide, snapshot.cells[1].kind); try testing.expectEqual(contracts.CellKind.continuation, snapshot.cells[2].kind); try testing.expect(!snapshot.cursor.visible); try testing.expectEqual(contracts.CursorShape.bar, snapshot.cursor.shape); try grid.feed("Z"); try testing.expectEqualStrings("A", snapshot.cells[0].text);}
test "checkpoint round trip preserves complete fragmented parser state" { var live = try Grid.init(testing.allocator, 12, 4); defer live.deinit(); try live.feed("normal\x1b[?1049h\x1b[31mALT\x1b[2;4r\x1b[?6h"); try live.feed("\x1b]8;;https://example.com\x1b\\X"); try live.feed("\x1b[12;");
const payload = try live.checkpointPayload(testing.allocator); defer testing.allocator.free(payload); var restored = try Grid.restoreCheckpoint(testing.allocator, payload); defer restored.deinit(); const repeated = try restored.checkpointPayload(testing.allocator); defer testing.allocator.free(repeated); try testing.expectEqualSlices(u8, payload, repeated);
try live.feed("3H\xe7\x95"); try restored.feed("3H\xe7\x95"); const fragmented = try restored.checkpointPayload(testing.allocator); defer testing.allocator.free(fragmented); var fragmented_restore = try Grid.restoreCheckpoint( testing.allocator, fragmented, ); defer fragmented_restore.deinit(); try live.feed("\x8c\x1b[?1049l"); try restored.feed("\x8c\x1b[?1049l"); try fragmented_restore.feed("\x8c\x1b[?1049l"); const expected = try live.checkpointPayload(testing.allocator); defer testing.allocator.free(expected); const actual = try restored.checkpointPayload(testing.allocator); defer testing.allocator.free(actual); const fragmented_actual = try fragmented_restore.checkpointPayload( testing.allocator, ); defer testing.allocator.free(fragmented_actual); try testing.expectEqualSlices(u8, expected, actual); try testing.expectEqualSlices(u8, expected, fragmented_actual);}
test "checkpoint rejects unsupported revision corruption and trailing bytes" { var grid = try Grid.init(testing.allocator, 4, 2); defer grid.deinit(); try grid.feed("ok"); const payload = try grid.checkpointPayload(testing.allocator); defer testing.allocator.free(payload);
const unsupported = try testing.allocator.dupe(u8, payload); defer testing.allocator.free(unsupported); std.mem.writeInt(u16, unsupported[4..6], checkpoint_schema_revision + 1, .little); try testing.expectError( error.UnsupportedEngineRevision, Grid.restoreCheckpoint(testing.allocator, unsupported), ); try testing.expectError( error.InvalidEngineCheckpoint, Grid.restoreCheckpoint(testing.allocator, payload[0 .. payload.len - 1]), ); const trailing = try std.mem.concat(testing.allocator, u8, &.{ payload, "x" }); defer testing.allocator.free(trailing); try testing.expectError( error.InvalidEngineCheckpoint, Grid.restoreCheckpoint(testing.allocator, trailing), );}
test "checkpoint preserves fragmented OSC DCS and synchronized updates" { var sync_live = try Grid.init(testing.allocator, 10, 2); defer sync_live.deinit(); try sync_live.feed("\x1b[?2026hbuffered"); const sync_payload = try sync_live.checkpointPayload(testing.allocator); defer testing.allocator.free(sync_payload); var sync_restored = try Grid.restoreCheckpoint( testing.allocator, sync_payload, ); defer sync_restored.deinit(); try sync_live.feed("\x1b[?2026l"); try sync_restored.feed("\x1b[?2026l"); try expectRow(sync_restored, 1, "buffered");
var strings = try Grid.init(testing.allocator, 10, 2); defer strings.deinit(); try strings.feed("\x1b]8;;https://partial"); const osc_payload = try strings.checkpointPayload(testing.allocator); defer testing.allocator.free(osc_payload); var osc_restored = try Grid.restoreCheckpoint(testing.allocator, osc_payload); defer osc_restored.deinit(); try osc_restored.feed(".example\x1b\\X\x1bP$q"); const dcs_payload = try osc_restored.checkpointPayload(testing.allocator); defer testing.allocator.free(dcs_payload); var dcs_restored = try Grid.restoreCheckpoint(testing.allocator, dcs_payload); defer dcs_restored.deinit(); var dcs_result = try dcs_restored.feedMode("m\x1b\\", .native_live); defer dcs_result.deinit(testing.allocator); try testing.expectEqual(@as(usize, 1), dcs_result.replies.items.len); try testing.expectEqualStrings("\x1bP1$r0m\x1b\\", dcs_result.replies.items[0].bytes);}
test "parser and reply collections enforce fixed bounds" { var grid = try Grid.init(testing.allocator, 10, 2); defer grid.deinit(); try testing.expectError( error.TooManyCsiParameters, grid.feed("\x1b[1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1H"), );
var replies = try Grid.init(testing.allocator, 10, 2); defer replies.deinit(); try testing.expectError( error.ReplyEffectCapacityExceeded, replies.feedMode("\x1b[5n" ** 17, .native_live), );
var osc = try Grid.init(testing.allocator, 10, 2); defer osc.deinit(); const oversized = try testing.allocator.alloc(u8, max_string_bytes + 3); defer testing.allocator.free(oversized); oversized[0] = 0x1b; oversized[1] = ']'; @memset(oversized[2..], 'x'); try testing.expectError(error.ControlStringTooLarge, osc.feed(oversized));}
fn checkOwnedEngineAllocationFailures(alloc: Allocator) !void { var grid = try Grid.init(alloc, 12, 3); defer grid.deinit(); var result = try grid.feedMode("\x1b[6n", .native_live); defer result.deinit(alloc); var snapshot = try grid.renderSnapshot(alloc); defer snapshot.deinit(alloc); const payload = try grid.checkpointPayload(alloc); defer alloc.free(payload); var restored = try Grid.restoreCheckpoint(alloc, payload); defer restored.deinit();}
test "owned effects snapshots and checkpoints handle allocation failure" { try testing.checkAllAllocationFailures( testing.allocator, checkOwnedEngineAllocationFailures, .{}, );}
test "bounded deterministic parser and checkpoint fuzz" { var random = std.Random.DefaultPrng.init(0x46585445); const source = random.random(); var bytes: [96]u8 = undefined; for (0..256) |_| { source.bytes(&bytes); const length = source.uintLessThan(usize, bytes.len + 1); var grid = try Grid.init(testing.allocator, 24, 8); defer grid.deinit(); var offset: usize = 0; while (offset < length) { const fragment = @min( length - offset, source.uintLessThan(usize, 8) + 1, ); var result = grid.feedMode( bytes[offset .. offset + fragment], .journal_replay, ) catch break; result.deinit(testing.allocator); offset += fragment; } const payload = grid.checkpointPayload(testing.allocator) catch continue; defer testing.allocator.free(payload); var restored = try Grid.restoreCheckpoint(testing.allocator, payload); defer restored.deinit(); const repeated = try restored.checkpointPayload(testing.allocator); defer testing.allocator.free(repeated); try testing.expectEqualSlices(u8, payload, repeated); }}
test "bounded deterministic corrupt checkpoint fuzz" { var base = try Grid.init(testing.allocator, 24, 8); defer base.deinit(); try base.feed("\x1b[31mcheckpoint\xe7\x95\x8c\x1b[?1049hALT"); const payload = try base.checkpointPayload(testing.allocator); defer testing.allocator.free(payload); var random = std.Random.DefaultPrng.init(0x46584350); const source = random.random(); for (0..256) |_| { const length = source.uintLessThan(usize, payload.len + 17); const candidate = try testing.allocator.alloc(u8, length); defer testing.allocator.free(candidate); source.bytes(candidate); const copied = @min(candidate.len, payload.len); @memcpy(candidate[0..copied], payload[0..copied]); if (candidate.len != 0) { const changes = source.uintLessThan(usize, 4) + 1; for (0..changes) |_| { candidate[source.uintLessThan(usize, candidate.len)] ^= source.int(u8) | 1; } } var restored = Grid.restoreCheckpoint( testing.allocator, candidate, ) catch continue; restored.deinit(); }}