Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
157 kB · 4337 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338// Host contract:// - fields: shadow_vt, transcript, entries, layout, viewport_top_row,// owned_top_row, pending_scroll_compact,// min_visible_viewport_rows, reflow_clear_guard_rows,// viewport_clear_pending,// replaceable_last_line, replaceable_row, replaceable_start,// last_rendered_cols, last_paint_bottom_reserved_rows,// last_visible_transcript_*, last_viewport_selection,// paint_trace, has_painted_transcript, resize_bottom_anchor_pending,// footer_viewport, full_transcript, folded_command_blocks// - methods: ensurePaintReservation, beginPaint, endPaint,// resetCursorTracking, advanceCursor,// compactAfterCollapsedResize, footerTopRowForExtra
const std = @import("std");const debug_trace = @import("../../core/shared/debug_trace.zig");const shared_theme = @import("../../core/shared/theme.zig");const types = @import("../../core/shared/types.zig");const command_output_content = @import("../../core/tooling/command_output_content.zig");const render_engine = @import("../render_engine.zig");const source_preparation = @import("source_preparation.zig");const transcript_release = @import("../../core/output/transcript_release.zig");const transcript_writer = @import("writer.zig");const vt_emulator = @import("../../core/terminal/engine.zig");
const Allocator = std.mem.Allocator;const Metrics = types.Metrics;const frame_surface = render_engine.frame_surface;const paint_plan = render_engine.paint_plan;const transcript_blocks = render_engine.transcript_blocks;const viewport_selection = render_engine.viewport_selection;
const HardLineStarts = viewport_selection.HardLineStarts;const LineRef = viewport_selection.LineRef;const TranscriptBuffer = viewport_selection.TranscriptBuffer;const TranscriptRef = viewport_selection.TranscriptRef;const VisibleTranscriptLine = viewport_selection.VisibleTranscriptLine;const VisibleTranscriptBatch = viewport_selection.VisibleTranscriptBatch;const ViewportSelection = viewport_selection.ViewportSelection;const buildHardLineStarts = viewport_selection.buildHardLineStarts;const hardLineRefAt = viewport_selection.hardLineRefAt;const selectedSourceLineIndex = viewport_selection.selectedSourceLineIndex;const visibleFromTranscript = viewport_selection.visibleFromTranscript;const buildViewportSelection = viewport_selection.buildViewportSelection;const isVisuallyBlankLine = transcript_blocks.isVisuallyBlankLine;const nextCursorColForLine = transcript_blocks.nextCursorColForLine;const skipVisualRowsInLine = transcript_blocks.skipVisualRowsInLine;const stripTrailingNewline = transcript_blocks.stripTrailingNewline;const visualRowsForLine = transcript_blocks.visualRowsForLine;
const blank_transcript_row_bytes = "\x1b[2K";
const ResizeHistoryPolicy = enum { apply, skip,};
fn rowForVisibleLineIndex( selection: ViewportSelection, line_visual_rows: []const u16, target_line: usize, max_row: u16,) ?u16 { if (target_line >= line_visual_rows.len) return null;
if (selection.split_active) { const prefix_lines = selection.split_prefix_lines; const suffix_start = selection.split_suffix_start_line; var row = selection.top_row; var line_index: usize = 0; const target_visible = if (target_line < prefix_lines) target_line else blk: { if (target_line < suffix_start) return null; while (line_index < prefix_lines) : (line_index += 1) { row +|= line_visual_rows[line_index]; } line_index = suffix_start; break :blk target_line; };
while (line_index < target_visible) : (line_index += 1) { row +|= line_visual_rows[line_index]; } if (row > max_row) return null; return row; }
if (target_line < selection.start_line) return null;
var row = selection.top_row; var line_index = selection.start_line; var skip_rows = selection.partial_skip_rows; while (line_index < target_line) : (line_index += 1) { const rows = line_visual_rows[line_index]; if (skip_rows >= rows) { skip_rows -= rows; continue; } row +|= rows - skip_rows; skip_rows = 0; } if (row > max_row) return null; return row;}
const RenderedTranscriptRow = struct { row: u16, line_index: usize, bytes: []const u8, rows_painted: u16, partial_skip_rows: u16, folded: bool, blank: bool,};
const RenderedTranscriptRows = struct { rows: std.ArrayList(RenderedTranscriptRow) = .empty, owned_bytes: std.ArrayList([]u8) = .empty, first_row: u16 = 0, last_visible_line: []const u8 = "", last_visible_row: u16 = 0, last_visible_row_blank: bool = false, last_line_is_folded: bool = false, replaceable_start_row: u16 = 0, replaceable_line_index: usize = 0, painted_rows: u16 = 0,
fn deinit(self: *RenderedTranscriptRows, alloc: Allocator) void { for (self.owned_bytes.items) |bytes| alloc.free(bytes); self.owned_bytes.deinit(alloc); self.rows.deinit(alloc); }
fn rememberOwnedBytes(self: *RenderedTranscriptRows, alloc: Allocator, bytes: []u8) ![]const u8 { errdefer alloc.free(bytes); try self.owned_bytes.append(alloc, bytes); return bytes; }};
fn applyRenderedRowsToSelection(selection: *ViewportSelection, rendered_rows: *const RenderedTranscriptRows) void { selection.last_visible_row = rendered_rows.last_visible_row; selection.last_visible_row_blank = rendered_rows.last_visible_row_blank; selection.replaceable_start_row = rendered_rows.replaceable_start_row; if (rendered_rows.last_visible_row > 0) { selection.bottom_row = rendered_rows.last_visible_row; }}
const ViewportCursorResult = struct { cursor_row: u16, cursor_col: u16, replaceable_row: u16,};
const TranscriptPreparationSource = source_preparation.TranscriptPreparationSource;
pub const MeasuredHistoryEndpoint = struct { span_start_flow_offset: usize, flow_offset: usize, hard_row_cols: u16,};
pub const MeasuredHistoryOrigin = struct { source_cols: u16, source_visual_offset: u32, endpoint: ?MeasuredHistoryEndpoint = null,};
pub const MeasuredHistoryOriginProjection = struct { origin: MeasuredHistoryOrigin, current_cols: u16,};
pub const MeasuredResizeTargetProvenance = enum { none, stable, recovering,};
fn renderTranscriptRows( self: anytype, alloc: Allocator, batch: VisibleTranscriptBatch, line_visual_rows: []const u16, total_lines: usize, start_line: usize, partial_skip_rows: u16, top_row: u16, bottom_row: u16, max_visual_rows: ?u16, projection_resume_bytes: []const u8, projection_start_flow_offset: ?usize,) !RenderedTranscriptRows { var rendered: RenderedTranscriptRows = .{ .first_row = top_row, .replaceable_start_row = top_row, .replaceable_line_index = total_lines, }; errdefer rendered.deinit(alloc);
var current_row: u32 = top_row; var line_index: usize = start_line; var remaining_visual_rows = max_visual_rows; if (remaining_visual_rows == null and projection_start_flow_offset != null) { remaining_visual_rows = bottom_row - top_row + 1; } while (line_index < total_lines and current_row <= @as(u32, bottom_row) and (remaining_visual_rows == null or remaining_visual_rows.? > 0)) : (line_index += 1) { const vis = batch.lines[line_index]; const is_partial_tail = line_index == start_line and partial_skip_rows > 0; const line_rows = line_visual_rows[line_index]; std.debug.assert(!is_partial_tail or partial_skip_rows < line_rows); const exact_tail = if (line_index == start_line) switch (vis) { .transcript => |line| projectionTailFromFlowOffset( batch, line.ref.ref, projection_start_flow_offset, ), .folded_line => null, } else null; const available_rows: u16 = if (exact_tail) |tail| visualRowsForLine(tail, self.layout.cols) else if (is_partial_tail) line_rows - partial_skip_rows else line_rows; const rows_painted = if (remaining_visual_rows) |remaining| @min(available_rows, remaining) else available_rows; const projection_truncates_line = rows_painted < available_rows; const paint_row: u16 = @intCast(current_row);
switch (vis) { .transcript => |line| { const text = batch.resolve(vis); const tail = exact_tail orelse transcriptRowBytes(self, text, is_partial_tail, partial_skip_rows); const visible_bytes = if (projection_truncates_line) tail[0..skipVisualRowsInLine(tail, self.layout.cols, rows_painted)] else tail; const paint_bytes = if (visible_bytes.len == 0) blank_transcript_row_bytes else if (isVisuallyBlankLine(visible_bytes)) blk: { const materialized = try std.mem.concat( alloc, u8, &.{ visible_bytes, blank_transcript_row_bytes }, ); break :blk try rendered.rememberOwnedBytes(alloc, materialized); } else visible_bytes; var emitted_bytes = paint_bytes; if (line_index == start_line and projection_resume_bytes.len > 0) { const resumed = try alloc.alloc( u8, projection_resume_bytes.len + paint_bytes.len, ); @memcpy(resumed[0..projection_resume_bytes.len], projection_resume_bytes); @memcpy(resumed[projection_resume_bytes.len..], paint_bytes); emitted_bytes = try rendered.rememberOwnedBytes(alloc, resumed); } const cursor_line = if (projection_truncates_line or exact_tail != null) visible_bytes else text; const blank = isVisuallyBlankLine(if (exact_tail != null) cursor_line else text); logRenderedTranscriptRow(paint_row, rows_painted, line_index, "transcript", if (is_partial_tail) partial_skip_rows else 0, text); try rendered.rows.append(alloc, .{ .row = paint_row, .line_index = line_index, .bytes = emitted_bytes, .rows_painted = rows_painted, .partial_skip_rows = if (is_partial_tail) partial_skip_rows else 0, .folded = false, .blank = blank, }); if (self.replaceable_last_line and line.ref.ref.start <= self.replaceable_start) { rendered.replaceable_line_index = line_index; } rendered.last_visible_line = cursor_line; rendered.last_line_is_folded = false; rendered.last_visible_row_blank = blank; }, .folded_line => |folded| { var folded_bytes = try foldedLineBytes(alloc, folded.text, folded.stream); var folded_bytes_needs_free = true; errdefer if (folded_bytes_needs_free) alloc.free(folded_bytes); const rows_touched = visualRowsForLine(folded_bytes, self.layout.cols); if (rows_touched < rows_painted) { const padded = try padAnsiBytesToRows(alloc, folded_bytes, rows_painted - rows_touched); alloc.free(folded_bytes); folded_bytes = padded; } folded_bytes_needs_free = false; const owned = try rendered.rememberOwnedBytes(alloc, folded_bytes); const tail = if (is_partial_tail) foldedPartialTailBytes(self, owned, partial_skip_rows) else owned; const bytes = if (projection_truncates_line) tail[0..skipVisualRowsInLine(tail, self.layout.cols, rows_painted)] else tail; logRenderedTranscriptRow( paint_row, rows_painted, line_index, if (folded.stream == .stderr) "folded_stderr" else "folded_stdout", if (is_partial_tail) partial_skip_rows else 0, folded.text, ); try rendered.rows.append(alloc, .{ .row = paint_row, .line_index = line_index, .bytes = if (bytes.len == 0) blank_transcript_row_bytes else bytes, .rows_painted = rows_painted, .partial_skip_rows = if (is_partial_tail) partial_skip_rows else 0, .folded = true, .blank = false, }); rendered.last_visible_line = if (projection_truncates_line) bytes else ""; rendered.last_line_is_folded = !projection_truncates_line; rendered.last_visible_row_blank = false; }, }
if (line_index == rendered.replaceable_line_index) { rendered.replaceable_start_row = paint_row; } rendered.last_visible_row = @intCast( current_row + @as(u32, rows_painted) - 1, ); rendered.painted_rows += rows_painted; current_row += @as(u32, rows_painted); if (remaining_visual_rows) |*remaining| remaining.* -= rows_painted; }
return rendered;}
fn projectionTailFromFlowOffset( batch: VisibleTranscriptBatch, line: LineRef, flow_offset: ?usize,) ?[]const u8 { const offset = flow_offset orelse return null; const start: usize = line.start; const end = start + @as(usize, line.len); if (offset < start or offset > end) return null; return batch.transcript.bytes[offset..end];}
fn transcriptRowBytes(self: anytype, text: []const u8, is_partial_tail: bool, partial_skip_rows: u16) []const u8 { if (!is_partial_tail or text.len == 0) return text;
const offset = skipVisualRowsInLine(text, self.layout.cols, partial_skip_rows); const tail_bytes: usize = if (offset < text.len) text.len - offset else 0; debug_trace.logf( "transcript.sgr_drop_partial_tail", "active sgr before skip offset not walked, rows_skipped={d} cols={d} tail_bytes={d}", .{ partial_skip_rows, self.layout.cols, tail_bytes }, ); return if (offset < text.len) text[offset..] else "";}
fn foldedPartialTailBytes(self: anytype, bytes: []const u8, partial_skip_rows: u16) []const u8 { if (partial_skip_rows == 0 or bytes.len == 0) return bytes; const offset = skipVisualRowsInLine(bytes, self.layout.cols, partial_skip_rows); return if (offset < bytes.len) bytes[offset..] else "";}
fn foldedLineBytes(alloc: Allocator, text: []const u8, stream: command_output_content.Stream) ![]u8 { const theme = shared_theme.current(); const style = if (stream == .stderr) theme.tool_stderr_style else theme.tool_stdout_style; const trimmed = stripTrailingNewline(text); const reset = "\x1b[0m"; const bytes = try alloc.alloc(u8, style.len + "│ ".len + trimmed.len + reset.len); var offset: usize = 0; @memcpy(bytes[offset .. offset + style.len], style); offset += style.len; @memcpy(bytes[offset .. offset + "│ ".len], "│ "); offset += "│ ".len; @memcpy(bytes[offset .. offset + trimmed.len], trimmed); offset += trimmed.len; @memcpy(bytes[offset .. offset + reset.len], reset); return bytes;}
fn padAnsiBytesToRows(alloc: Allocator, bytes: []const u8, missing_rows: u16) ![]u8 { std.debug.assert(missing_rows > 0); return std.fmt.allocPrint(alloc, "{s}\x1b[{d}B\x1b[2K", .{ bytes, missing_rows });}
fn logRenderedTranscriptRow(row: u16, rows_painted: u16, line_index: usize, kind_name: []const u8, partial_skip_rows: u16, text: []const u8) void { debug_trace.logf( "paint.rows", "transcript_line row={d} rows={d} line_index={d} kind={s} partial_skip={d} bytes={d} hash={x}", .{ row, rows_painted, line_index, kind_name, partial_skip_rows, text.len, std.hash.Wyhash.hash(0, text), }, );}
fn calculateViewportCursor( layout: types.Layout, bottom_row: u16, total_lines: usize, start_line: usize, repl_line_idx: usize, replaceable_start_row: u16, last_visible_line: []const u8, last_visible_row: u16, last_line_is_folded: bool, top_row: u16, transcript_ends_with_newline: bool, replaceable_last_line: bool, current_replaceable_row: u16,) ViewportCursorResult { _ = bottom_row; var cursor_row = top_row; var cursor_col: u16 = 1; var replaceable_row = current_replaceable_row; if (total_lines == 0) return .{ .cursor_row = cursor_row, .cursor_col = cursor_col, .replaceable_row = replaceable_row };
const treat_last_row_as_terminated = last_line_is_folded or transcript_ends_with_newline; if (treat_last_row_as_terminated and last_visible_row < layout.content_bottom) { cursor_row = last_visible_row + 1; cursor_col = 1; } else { cursor_row = last_visible_row; cursor_col = nextCursorColForLine(last_visible_line, layout.cols); } if (cursor_row < 1) cursor_row = 1; if (cursor_col < 1) cursor_col = 1; if (replaceable_last_line) { replaceable_row = if (repl_line_idx < start_line or repl_line_idx >= total_lines) top_row else replaceable_start_row; } return .{ .cursor_row = cursor_row, .cursor_col = cursor_col, .replaceable_row = replaceable_row };}
fn recordPreFrameTranscriptInvalidation( self: anytype, reason: paint_plan.FrameInvalidationReason, top: u16, bottom: u16,) void { if (top == 0 or bottom == 0 or bottom < top) return; const Shell = @TypeOf(self.*); if (comptime !@hasDecl(Shell, "recordFrameInvalidation")) return; self.recordFrameInvalidation(.{ .reason = reason, .top = top, .bottom = bottom }) catch |err| { debug_trace.logf( "frame_plan", "pre_frame_transcript_invalidation_drop reason={s} top={d} bottom={d} err={s}", .{ @tagName(reason), top, bottom, @errorName(err) }, ); };}
fn compactAfterCollapsedResize(self: anytype, metrics: *Metrics) !void { _ = metrics; if (!self.pending_scroll_compact) return; self.pending_scroll_compact = false; if (self.owned_top_row <= 1) return;
const scroll_rows = self.owned_top_row - 1; debug_trace.logf("render", "viewport_scroll_compact rows={d} owned_top={d} layout={d}x{d}", .{ scroll_rows, self.owned_top_row, self.layout.cols, self.layout.rows });
self.owned_top_row = 1; self.viewport_top_row = 1; self.cursor_row = 1; self.cursor_col = 1; recordPreFrameTranscriptInvalidation(self, .viewport_reanchor, 1, self.layout.rows); debug_trace.logf( "scroll", "viewport_scroll_compact_frame_deferred rows={d} layout={d}x{d}", .{ scroll_rows, self.layout.cols, self.layout.rows }, );}
fn visualOffsetForSelection(line_visual_rows: []const u16, selection: ViewportSelection) u32 { var offset: u32 = 0; const end = @min(selection.start_line, line_visual_rows.len); for (line_visual_rows[0..end]) |rows| offset += rows; return offset + selection.partial_skip_rows;}
fn totalVisualRows(line_visual_rows: []const u16) u32 { var total: u32 = 0; for (line_visual_rows) |rows| total += rows; return total;}
const VisualRowPosition = struct { line: usize, intra_line_rows: u16,};
fn compareVisualOffset(target: u32, offset: u32) std.math.Order { return std.math.order(target, offset);}
fn sourceStartPosition( prepared: *const PreparedTranscriptSurfacePaint, visual_offset: u32,) ?VisualRowPosition { const offsets = prepared.source_visual_row_offsets; if (offsets.len == prepared.sourceVisualRows().len + 1 and offsets.len > 0) { if (visual_offset > offsets[offsets.len - 1]) return null; const line = std.sort.upperBound( u32, offsets, visual_offset, compareVisualOffset, ) - 1; return .{ .line = line, .intra_line_rows = if (line < prepared.sourceVisualRows().len) @intCast(visual_offset - offsets[line]) else 0, }; }
var remaining = visual_offset; for (prepared.sourceVisualRows(), 0..) |rows, line| { if (remaining < rows) return .{ .line = line, .intra_line_rows = @intCast(remaining) }; remaining -= rows; } if (remaining == 0) return .{ .line = prepared.sourceVisualRows().len, .intra_line_rows = 0 }; return null;}
fn sourceEndPosition( prepared: *const PreparedTranscriptSurfacePaint, visual_end: u32,) ?VisualRowPosition { if (visual_end == 0) return .{ .line = 0, .intra_line_rows = 0 }; const offsets = prepared.source_visual_row_offsets; if (offsets.len == prepared.sourceVisualRows().len + 1 and offsets.len > 1) { if (visual_end > offsets[offsets.len - 1]) return null; const line = std.sort.lowerBound( u32, offsets[1..], visual_end, compareVisualOffset, ); return .{ .line = line, .intra_line_rows = @intCast(visual_end - offsets[line]), }; }
var remaining = visual_end; for (prepared.sourceVisualRows(), 0..) |rows, line| { if (remaining <= rows) return .{ .line = line, .intra_line_rows = @intCast(remaining) }; remaining -= rows; } return null;}
const MeasuredSourceRow = struct { start: usize, end: usize, next: usize,};
fn selectionFlowOffset( batch: VisibleTranscriptBatch, selection: ViewportSelection, cols: u16,) ?usize { if (selection.split_active or selection.start_line > batch.lines.len) return null; if (selection.start_line == batch.lines.len) return batch.transcript.bytes.len; const line = switch (batch.lines[selection.start_line]) { .transcript => |value| value, .folded_line => return null, }; const text = line.ref.resolve(batch.transcript); return @as(usize, line.ref.ref.start) + skipVisualRowsInLine(text, cols, selection.partial_skip_rows);}
fn nextTranscriptLineOffset( batch: VisibleTranscriptBatch, line_index: usize, fallback: usize,) ?usize { if (line_index + 1 >= batch.lines.len) return fallback; return switch (batch.lines[line_index + 1]) { .transcript => |line| line.ref.ref.start, .folded_line => null, };}
fn collectMeasuredSourceRows( alloc: Allocator, batch: VisibleTranscriptBatch, hard_row_cols: u16, span_start: usize, span_end: usize,) !?[]MeasuredSourceRow { if (hard_row_cols == 0 or span_start > span_end or span_end > batch.transcript.bytes.len) { return null; }
var groups: std.ArrayList(MeasuredSourceRow) = .empty; defer groups.deinit(alloc); for (batch.lines, 0..) |visible, line_index| { const line = switch (visible) { .transcript => |value| value, .folded_line => return null, }; const text = line.ref.resolve(batch.transcript); const line_start: usize = line.ref.ref.start; const rows = visualRowsForLine(text, hard_row_cols); var row: u16 = 0; while (row < rows) : (row += 1) { const local_start = skipVisualRowsInLine(text, hard_row_cols, row); const local_end = skipVisualRowsInLine(text, hard_row_cols, row + 1); const group_start = line_start + local_start; const group_end = line_start + local_end; const group_next = if (row + 1 == rows) nextTranscriptLineOffset(batch, line_index, group_end) orelse return null else group_end;
if (group_next <= span_start) continue; if (group_start >= span_end) break; const effective_start = @max(group_start, span_start); const effective_end = @min(group_end, span_end); const effective_next = @min(group_next, span_end); if (effective_start > effective_end or effective_end > effective_next) return null; try groups.append(alloc, .{ .start = effective_start, .end = effective_end, .next = effective_next, }); if (effective_next == span_end) { return try groups.toOwnedSlice(alloc); } } } if (span_start == span_end) return try groups.toOwnedSlice(alloc); return null;}
/// Finds the byte offset after `rows` visual rows. Shared endpoint logic keeps/// forward and reverse projection walks inverse.fn flowOffsetAfterMeasuredRows( batch: VisibleTranscriptBatch, groups: []const MeasuredSourceRow, target_cols: u16, span_start: usize, rows: u32,) ?usize { var remaining = rows; var offset = span_start; for (groups) |group| { const bytes = batch.transcript.bytes[group.start..group.end]; const group_rows = visualRowsForLine(bytes, target_cols); if (remaining < group_rows) { return group.start + skipVisualRowsInLine( bytes, target_cols, @intCast(remaining), ); } remaining -= group_rows; offset = group.next; if (remaining == 0) break; } return if (remaining == 0) offset else null;}
fn advanceMeasuredHistoryEndpoint( alloc: Allocator, batch: VisibleTranscriptBatch, selection: ViewportSelection, source_cols: u16, target_cols: u16, rows: u32,) !?MeasuredHistoryEndpoint { const span_start = selectionFlowOffset(batch, selection, source_cols) orelse return null; return advanceMeasuredHistoryEndpointFromFlowOffset( alloc, batch, source_cols, target_cols, span_start, batch.transcript.bytes.len, rows, );}
fn advanceMeasuredHistoryEndpointFromFlowOffset( alloc: Allocator, batch: VisibleTranscriptBatch, source_cols: u16, target_cols: u16, span_start: usize, span_end: usize, rows: u32,) !?MeasuredHistoryEndpoint { const groups = (try collectMeasuredSourceRows( alloc, batch, source_cols, span_start, span_end, )) orelse return null; defer alloc.free(groups);
const flow_offset = flowOffsetAfterMeasuredRows( batch, groups, target_cols, span_start, rows, ) orelse return null; return .{ .span_start_flow_offset = span_start, .flow_offset = flow_offset, .hard_row_cols = source_cols, };}
fn measuredHistoryFlowOffsetForVisualOffset( alloc: Allocator, batch: VisibleTranscriptBatch, source_cols: u16, visual_offset: u32,) !?usize { const span_end = measuredHistorySourceSpanEnd(batch); const groups = (try collectMeasuredSourceRows( alloc, batch, source_cols, 0, span_end, )) orelse return null; defer alloc.free(groups);
const index: usize = @intCast(visual_offset); if (index > groups.len) return null; if (index == groups.len) return span_end; return groups[index].start;}
fn measuredHistorySourceSpanEnd(batch: VisibleTranscriptBatch) usize { const bytes = batch.transcript.bytes; return if (bytes.len > 0 and bytes[bytes.len - 1] == '\n') bytes.len - 1 else bytes.len;}
fn advanceMeasuredHistoryVisualOffset( alloc: Allocator, batch: VisibleTranscriptBatch, source_cols: u16, target_cols: u16, visual_offset: u32, rows: u32,) !?MeasuredHistoryEndpoint { const span_start = (try measuredHistoryFlowOffsetForVisualOffset( alloc, batch, source_cols, visual_offset, )) orelse return null; return advanceMeasuredHistoryEndpointFromFlowOffset( alloc, batch, source_cols, target_cols, span_start, measuredHistorySourceSpanEnd(batch), rows, );}
fn rewindMeasuredHistoryEndpoint( alloc: Allocator, batch: VisibleTranscriptBatch, endpoint: MeasuredHistoryEndpoint, target_cols: u16, rows: u32,) !?usize { const groups = (try collectMeasuredSourceRows( alloc, batch, endpoint.hard_row_cols, endpoint.span_start_flow_offset, endpoint.flow_offset, )) orelse return null; defer alloc.free(groups);
var total: u32 = 0; for (groups) |group| { const bytes = batch.transcript.bytes[group.start..group.end]; total += visualRowsForLine(bytes, target_cols); } if (rows > total) return null; return flowOffsetAfterMeasuredRows( batch, groups, target_cols, endpoint.span_start_flow_offset, total - rows, );}
pub fn advanceMeasuredHistoryOriginForCommittedRows( flow: []const u8, origin: ?MeasuredHistoryOrigin, target_cols: u16, rows: u16,) ?MeasuredHistoryOrigin { var advanced = origin orelse return null; if (rows == 0) return advanced; const endpoint = advanced.endpoint orelse return advanced; advanced.endpoint = if (advanceFlowOffset( flow, endpoint, target_cols, rows, )) |flow_offset| .{ .span_start_flow_offset = endpoint.span_start_flow_offset, .flow_offset = flow_offset, .hard_row_cols = endpoint.hard_row_cols, } else null; return advanced;}
fn advanceFlowOffset( flow: []const u8, endpoint: MeasuredHistoryEndpoint, cols: u16, rows: u16,) ?usize { if (cols == 0 or endpoint.hard_row_cols == 0 or endpoint.span_start_flow_offset > endpoint.flow_offset or endpoint.flow_offset > flow.len) { return null; }
var offset = endpoint.flow_offset; var remaining: u32 = rows; var line_start: usize = 0; while (line_start < flow.len and remaining > 0) { const newline = std.mem.findScalar(u8, flow[line_start..], '\n'); const line_end = if (newline) |index| line_start + index else flow.len; const line = flow[line_start..line_end]; const source_rows = visualRowsForLine(line, endpoint.hard_row_cols);
var source_row: u16 = 0; while (source_row < source_rows and remaining > 0) : (source_row += 1) { const local_start = skipVisualRowsInLine(line, endpoint.hard_row_cols, source_row); const local_end = skipVisualRowsInLine(line, endpoint.hard_row_cols, source_row + 1); const group_start = line_start + local_start; const group_end = line_start + local_end; const group_next = if (source_row + 1 == source_rows and line_end < flow.len) line_end + 1 else group_end;
if (group_next <= endpoint.span_start_flow_offset or group_next <= offset) { continue; } const clipped_start = @max(group_start, endpoint.span_start_flow_offset); if (group_start == group_end) { if (offset <= clipped_start) { remaining -= 1; offset = group_next; } continue; } const segment_start = @max(clipped_start, offset); if (segment_start >= group_end) { offset = @max(offset, group_next); continue; }
const segment = flow[segment_start..group_end]; const segment_rows = visualRowsForLine(segment, cols); if (remaining < segment_rows) { offset = segment_start + skipVisualRowsInLine(segment, cols, @intCast(remaining)); remaining = 0; break; } remaining -= segment_rows; offset = group_next; }
if (newline == null) break; line_start = line_end + 1; } return if (remaining == 0) offset else null;}
fn visualOffsetForExactFlowOffset( batch: VisibleTranscriptBatch, line_visual_rows: []const u16, cols: u16, flow_offset: usize,) ?u32 { const projection = visualProjectionForFlowOffset( batch, line_visual_rows, cols, flow_offset, ) orelse return null; return if (projection.exact) projection.visual_offset else null;}
const FlowOffsetProjection = struct { visual_offset: u32, exact: bool,};
fn visualProjectionForFlowOffset( batch: VisibleTranscriptBatch, line_visual_rows: []const u16, cols: u16, flow_offset: usize,) ?FlowOffsetProjection { if (line_visual_rows.len != batch.lines.len) return null;
var visual_offset: u32 = 0; for (batch.lines, line_visual_rows) |visible, line_rows| { const line = switch (visible) { .transcript => |value| value, .folded_line => return null, }; const text = line.ref.resolve(batch.transcript); const line_start: usize = line.ref.ref.start; const line_end = line_start + text.len; if (text.len == 0 and flow_offset == line_start) { return .{ .visual_offset = visual_offset, .exact = true, }; } if (flow_offset >= line_start and flow_offset < line_end) { var row: u16 = 0; while (row < line_rows) : (row += 1) { const row_start = line_start + skipVisualRowsInLine(text, cols, row); const row_end = line_start + skipVisualRowsInLine(text, cols, row + 1); if (flow_offset < row_start) return null; if (flow_offset < row_end) { return .{ .visual_offset = visual_offset + row, .exact = flow_offset == row_start, }; } } return null; } if (flow_offset == line_end and line_end == batch.transcript.bytes.len) { return .{ .visual_offset = visual_offset + line_rows, .exact = true, }; } visual_offset += line_rows; } return if (flow_offset == batch.transcript.bytes.len) .{ .visual_offset = visual_offset, .exact = true } else null;}
fn normalizeSplitSelection( selection: *ViewportSelection, line_visual_rows: []const u16,) void { if (selection.split_active) { selection.start_line = selection.split_suffix_start_line; selection.partial_skip_rows = 0; selection.split_active = false; selection.split_prefix_lines = 0; selection.split_suffix_start_line = 0; } selection.line_count = line_visual_rows.len;}
fn setSelectionVisualOffset( selection: *ViewportSelection, line_visual_rows: []const u16, target_offset: u32,) void { normalizeSplitSelection(selection, line_visual_rows); var remaining = @min(target_offset, totalVisualRows(line_visual_rows)); var line_index: usize = 0; while (line_index < line_visual_rows.len and remaining >= line_visual_rows[line_index]) : (line_index += 1) { remaining -= line_visual_rows[line_index]; } selection.start_line = line_index; selection.partial_skip_rows = if (line_index < line_visual_rows.len) @intCast(remaining) else 0;}
fn applyResizeHistoryOffset( selection: *ViewportSelection, line_visual_rows: []const u16, target_offset: u32,) bool { if (target_offset == 0 or line_visual_rows.len == 0) return false; normalizeSplitSelection(selection, line_visual_rows); const current_offset = visualOffsetForSelection(line_visual_rows, selection.*); const applied_offset = @max(current_offset, target_offset); if (applied_offset == current_offset) return false; setSelectionVisualOffset(selection, line_visual_rows, applied_offset); return true;}
fn offsetBySignedRows(base: u32, delta: i32) u32 { if (delta >= 0) { return base +| @as(u32, @intCast(delta)); } return base -| @as(u32, @intCast(-@as(i64, delta)));}
fn preserveWelcomeReplayBoundary( target_offset: u32, row_delta: i32, welcome_boundary: ?transcript_blocks.LeadingWelcomeBoundary, line_visual_rows: []const u16,) u32 { if (target_offset == 0 or row_delta == 0) return target_offset; const boundary = welcome_boundary orelse return target_offset; var welcome_offset: u32 = 0; for (line_visual_rows[0..@min(boundary.full_cut_line, line_visual_rows.len)]) |rows| { welcome_offset += rows; } if (target_offset >= welcome_offset) return target_offset; if (row_delta > 0) return welcome_offset;
var tail_offset: u32 = 0; for (line_visual_rows[0..@min(boundary.tail_replay_line, line_visual_rows.len)]) |rows| { tail_offset += rows; } return tail_offset;}
fn unusedRowsForSelection( line_visual_rows: []const u16, selection: ViewportSelection, visible_rows: u16,) u16 { if (selection.start_line >= line_visual_rows.len) return visible_rows; var remaining = visible_rows; var line_index = selection.start_line; var skip_rows = selection.partial_skip_rows; while (line_index < line_visual_rows.len) : (line_index += 1) { const rows = line_visual_rows[line_index] -| skip_rows; skip_rows = 0; if (rows > remaining) return 0; remaining -= rows; } return remaining;}
pub const PreparedTranscriptSurfacePaint = struct { bytes: []u8 = &.{}, owns_bytes: bool = true, visible_lines: std.ArrayList(VisibleTranscriptLine) = .empty, line_visual_rows: std.ArrayList(u16) = .empty, line_provenance: std.ArrayList(transcript_blocks.LineProvenance) = .empty, source_visible_lines: []const VisibleTranscriptLine = &.{}, source_line_visual_rows: []const u16 = &.{}, source_line_provenance: []const transcript_blocks.LineProvenance = &.{}, source_visual_row_offsets: []const u32 = &.{}, selected_visible_lines: std.ArrayList(VisibleTranscriptLine) = .empty, selected_line_visual_rows: std.ArrayList(u16) = .empty, selected_line_provenance: std.ArrayList(transcript_blocks.LineProvenance) = .empty, row_provenance: std.ArrayList(transcript_blocks.RowProvenance) = .empty, use_selected: bool = false, selection: ViewportSelection, projection_area: render_engine.frame_layout.FrameRect = .empty(), projection_visual_rows: ?u16 = null, projection_ends_with_newline: ?bool = null, projection_cursor: ?ViewportCursorResult = null, projection_resume_bytes: []u8 = &.{}, projection_resume_flow_offset: ?usize = null, projection_resume_cursor_col: u16 = 1, projection_resume_pending_wrap: bool = false, projection_start_flow_offset: ?usize = null, total_lines: usize = 0, transcript_ends_with_newline: bool = false, bottom_reserved_rows: u16 = 0, tracked_entry_id: ?u32 = null, tracked_entry_row: ?u16 = null, replaceable_last_line: bool = false, replaceable_start: usize = 0, replaceable_row: u16 = 1, trace_transcript_frame: bool = false, trace_visible_rows: u16 = 0, measured_history_origin: ?MeasuredHistoryOrigin = null, measured_history_origin_consumed: bool = false, measured_resize_target_provenance: MeasuredResizeTargetProvenance = .none, // Owned copy of the source's finality boundary candidates; the scroll // planner selects the release floor from these against frame-fresh // producer facts. Empty candidates mean the whole flow is final. finality_candidates: transcript_release.Candidates = .{}, cursor: ViewportCursorResult = .{ .cursor_row = 1, .cursor_col = 1, .replaceable_row = 1 },
pub fn deinit(self: *PreparedTranscriptSurfacePaint, alloc: Allocator) void { self.finality_candidates.deinit(alloc); if (self.owns_bytes and self.bytes.len > 0) alloc.free(self.bytes); self.visible_lines.deinit(alloc); self.line_visual_rows.deinit(alloc); self.line_provenance.deinit(alloc); self.selected_visible_lines.deinit(alloc); self.selected_line_visual_rows.deinit(alloc); self.selected_line_provenance.deinit(alloc); self.row_provenance.deinit(alloc); if (self.projection_resume_bytes.len > 0) { alloc.free(self.projection_resume_bytes); } }
fn batch(self: *const PreparedTranscriptSurfacePaint) VisibleTranscriptBatch { return .{ .transcript = .{ .bytes = self.bytes }, .lines = if (self.use_selected) self.selected_visible_lines.items else self.sourceVisibleLines(), }; }
pub fn sourceVisibleLines(self: *const PreparedTranscriptSurfacePaint) []const VisibleTranscriptLine { return if (self.source_visible_lines.len > 0) self.source_visible_lines else self.visible_lines.items; }
pub fn visualRows(self: *const PreparedTranscriptSurfacePaint) []const u16 { return if (self.use_selected) self.selected_line_visual_rows.items else self.sourceVisualRows(); }
pub fn sourceVisualRows(self: *const PreparedTranscriptSurfacePaint) []const u16 { return if (self.source_line_visual_rows.len > 0) self.source_line_visual_rows else self.line_visual_rows.items; }
pub fn sourceTotalVisualRows(self: *const PreparedTranscriptSurfacePaint) u32 { if (self.source_visual_row_offsets.len > 0) { return self.source_visual_row_offsets[self.source_visual_row_offsets.len - 1]; } return totalVisualRows(self.sourceVisualRows()); }
pub fn sourceVisualOffset(self: *const PreparedTranscriptSurfacePaint) u32 { return self.sourceVisualOffsetFor(self.selection); }
fn sourceVisualOffsetFor( self: *const PreparedTranscriptSurfacePaint, selection: ViewportSelection, ) u32 { if (!selection.split_active and self.source_visual_row_offsets.len == self.sourceVisualRows().len + 1 and selection.start_line < self.source_visual_row_offsets.len) { return self.source_visual_row_offsets[selection.start_line] + selection.partial_skip_rows; } return visualOffsetForSelection(self.sourceVisualRows(), selection); }
fn lineProvenance(self: *const PreparedTranscriptSurfacePaint) []const transcript_blocks.LineProvenance { return if (self.use_selected) self.selected_line_provenance.items else if (self.source_line_provenance.len > 0) self.source_line_provenance else self.line_provenance.items; }
pub fn rowProvenance(self: *const PreparedTranscriptSurfacePaint, row: u16) ?transcript_blocks.LineProvenance { for (self.row_provenance.items) |item| { if (item.row == row) return item.source; } return null; }
pub fn projectionArea(self: *const PreparedTranscriptSurfacePaint) render_engine.frame_layout.FrameRect { if (!self.projection_area.isEmpty()) return self.projection_area; if (self.selection.top_row == 0 or self.selection.bottom_row < self.selection.top_row) { return .empty(); } return .{ .top = self.selection.top_row, .bottom = self.selection.bottom_row, }; }};
pub fn previewTranscriptFlow( self: anytype, alloc: Allocator,) !render_engine.frame_layout.TranscriptFlowPreview { var source = try source_preparation.prepareTranscriptSource(self, alloc, null); defer source.deinit(alloc); return source.preview;}
pub fn prepareTranscriptSurfacePaint( self: anytype, alloc: Allocator, metrics: *Metrics, bottom_reserved_rows: u16,) !PreparedTranscriptSurfacePaint { return prepareTranscriptSurfacePaintWithOwnedSource( self, alloc, metrics, bottom_reserved_rows, null, null, );}
pub fn prepareTranscriptSurfacePaintForArea( self: anytype, alloc: Allocator, metrics: *Metrics, area: render_engine.frame_layout.FrameRect,) !PreparedTranscriptSurfacePaint { return prepareTranscriptSurfacePaintWithOwnedSource( self, alloc, metrics, 0, area, null, );}
pub fn prepareTranscriptSurfacePaintForAreaTrackingEntry( self: anytype, alloc: Allocator, metrics: *Metrics, area: render_engine.frame_layout.FrameRect, entry_id: ?u32,) !PreparedTranscriptSurfacePaint { return prepareTranscriptSurfacePaintWithOwnedSource( self, alloc, metrics, 0, area, entry_id, );}
fn prepareTranscriptSurfacePaintWithOwnedSource( self: anytype, alloc: Allocator, metrics: *Metrics, bottom_reserved_rows: u16, solved_area: ?render_engine.frame_layout.FrameRect, tracked_entry_id: ?u32,) !PreparedTranscriptSurfacePaint { var source = try source_preparation.prepareTranscriptSource( self, alloc, tracked_entry_id, ); defer source.deinit(alloc);
var prepared = try prepareTranscriptSurfacePaintInternal( self, alloc, metrics, bottom_reserved_rows, solved_area, &source, true, false, .apply, null, ); if (source.bytes.len > 0) { prepared.owns_bytes = true; source.bytes = &.{}; } return prepared;}
pub fn prepareTranscriptSurfacePaintFromSourceForArea( self: anytype, alloc: Allocator, metrics: *Metrics, source: *const TranscriptPreparationSource, area: render_engine.frame_layout.FrameRect,) !PreparedTranscriptSurfacePaint { return prepareTranscriptSurfacePaintInternal( self, alloc, metrics, 0, area, source, false, false, .apply, null, );}
pub fn preparePreselectedTranscriptSurfacePaintFromSourceForArea( self: anytype, alloc: Allocator, metrics: *Metrics, source: *const TranscriptPreparationSource, area: render_engine.frame_layout.FrameRect,) !PreparedTranscriptSurfacePaint { return prepareTranscriptSurfacePaintInternal( self, alloc, metrics, 0, area, source, false, false, .skip, null, );}
pub fn prepareTranscriptSurfacePaintFromSourceForFrame( self: anytype, alloc: Allocator, metrics: *Metrics, source: *const TranscriptPreparationSource, area: render_engine.frame_layout.FrameRect, allow_projection_rebase: bool,) !PreparedTranscriptSurfacePaint { return prepareTranscriptSurfacePaintInternal( self, alloc, metrics, 0, area, source, false, allow_projection_rebase, .apply, null, );}
pub fn prepareIndexedFullTranscriptSurfacePaintForArea( self: anytype, alloc: Allocator, metrics: *Metrics, source: *const TranscriptPreparationSource, area: render_engine.frame_layout.FrameRect, visual_offset: u32,) !PreparedTranscriptSurfacePaint { return prepareTranscriptSurfacePaintInternal( self, alloc, metrics, 0, area, source, false, false, .skip, visual_offset, );}
const MeasuredEndpointMapping = struct { visual_offset: ?u32 = null, start_flow_offset: ?usize = null, trace_status: []const u8,};
fn mapMeasuredEndpointTarget( prepared: *PreparedTranscriptSurfacePaint, cols: u16, endpoint: MeasuredHistoryEndpoint,) MeasuredEndpointMapping { prepared.measured_history_origin.?.endpoint = endpoint; const projection = visualProjectionForFlowOffset( prepared.batch(), prepared.sourceVisualRows(), cols, endpoint.flow_offset, ) orelse return .{ .trace_status = "mapping_inexact" }; if (projection.exact) { return .{ .visual_offset = projection.visual_offset, .trace_status = "exact" }; } return .{ .visual_offset = projection.visual_offset, .start_flow_offset = endpoint.flow_offset, .trace_status = "containing_row", };}
fn resolveResizeHistoryTarget( self: anytype, alloc: Allocator, prepared: *PreparedTranscriptSurfacePaint, transcript_bytes: []const u8, source: *const TranscriptPreparationSource, start_flow_offset: *?usize,) !?u32 { if (comptime !@hasField(@TypeOf(self.*), "resize_history_row_delta")) return null; const row_delta = self.resize_history_row_delta orelse return null;
var committed_offset: u32 = 0; var exact_measured_target: ?u32 = null; var origin_projection: ?MeasuredHistoryOriginProjection = null; if (comptime @hasDecl(@TypeOf(self.*), "measuredHistoryOriginForFlow")) { origin_projection = self.measuredHistoryOriginForFlow(transcript_bytes); } var trace_endpoint_status: []const u8 = "none"; var trace_origin_action: []const u8 = "signed_fallback"; var trace_source_cols: u16 = 0; var trace_current_cols: u16 = 0; var trace_source_visual_offset: u32 = 0; var recovery_accepted_visual_offset: ?u32 = null; var recovery_source_cols: u16 = 0; if (comptime @hasDecl(@TypeOf(self.*), "measuredResizeProjectionForFlow")) { if (self.measuredResizeProjectionForFlow(transcript_bytes)) |projection| { switch (projection) { .stable => {}, .recovering => |recovering| { recovery_accepted_visual_offset = recovering.accepted_visual_offset; recovery_source_cols = recovering.source_cols; }, } } } if (comptime @hasDecl(@TypeOf(self.*), "stableTranscriptProjectionForFlow")) { if (self.stableTranscriptProjectionForFlow(transcript_bytes)) |stable| { prepared.measured_resize_target_provenance = .stable; if (!stable.selection.split_active) { committed_offset = prepared.sourceVisualOffsetFor(stable.selection); } if (row_delta > 0 and self.committed_frame_layout.terminal_cols > 0) { prepared.measured_history_origin = .{ .source_cols = self.committed_frame_layout.terminal_cols, .source_visual_offset = stable.visual_offset, }; if (!stable.selection.split_active) { if (try advanceMeasuredHistoryEndpoint( alloc, prepared.batch(), stable.selection, self.committed_frame_layout.terminal_cols, self.layout.cols, @intCast(row_delta), )) |endpoint| { const mapping = mapMeasuredEndpointTarget(prepared, self.layout.cols, endpoint); exact_measured_target = mapping.visual_offset; if (mapping.start_flow_offset) |flow_offset| start_flow_offset.* = flow_offset; trace_endpoint_status = mapping.trace_status; } else { trace_endpoint_status = "unavailable"; } } else { trace_endpoint_status = "unavailable"; } trace_origin_action = "retain"; trace_source_cols = self.committed_frame_layout.terminal_cols; trace_current_cols = self.committed_frame_layout.terminal_cols; trace_source_visual_offset = stable.visual_offset; } else if (row_delta < 0) { prepared.measured_history_origin_consumed = true; trace_origin_action = "consume"; if (stable.measured_history_origin) |origin| { trace_source_cols = origin.source_cols; trace_current_cols = self.committed_frame_layout.terminal_cols; trace_source_visual_offset = origin.source_visual_offset; trace_endpoint_status = if (origin.endpoint != null) "exact" else "unavailable"; if (!stable.selection.split_active) { if (origin.endpoint) |endpoint| { const flow_offset = try rewindMeasuredHistoryEndpoint( alloc, prepared.batch(), endpoint, self.layout.cols, @intCast(-@as(i64, row_delta)), ); if (flow_offset) |exact_flow_offset| { exact_measured_target = visualOffsetForExactFlowOffset( prepared.batch(), prepared.sourceVisualRows(), self.layout.cols, exact_flow_offset, ); } } } } } } } if (recovery_accepted_visual_offset) |accepted_visual_offset| { trace_origin_action = "recovery_rebase"; trace_source_cols = recovery_source_cols; trace_current_cols = recovery_source_cols; trace_source_visual_offset = accepted_visual_offset;
if (row_delta > 0) { prepared.measured_history_origin = .{ .source_cols = recovery_source_cols, .source_visual_offset = accepted_visual_offset, }; if (try advanceMeasuredHistoryVisualOffset( alloc, prepared.batch(), recovery_source_cols, self.layout.cols, accepted_visual_offset, @intCast(row_delta), )) |endpoint| { const mapping = mapMeasuredEndpointTarget(prepared, self.layout.cols, endpoint); exact_measured_target = mapping.visual_offset; if (mapping.start_flow_offset) |flow_offset| start_flow_offset.* = flow_offset; trace_endpoint_status = mapping.trace_status; } else { trace_endpoint_status = "unavailable"; } }
if (exact_measured_target == null) { if (try measuredHistoryFlowOffsetForVisualOffset( alloc, prepared.batch(), recovery_source_cols, accepted_visual_offset, )) |flow_offset| { if (visualOffsetForExactFlowOffset( prepared.batch(), prepared.sourceVisualRows(), self.layout.cols, flow_offset, )) |mapped_offset| { committed_offset = mapped_offset; trace_endpoint_status = "base_exact"; } } } if (exact_measured_target != null or committed_offset != 0 or accepted_visual_offset == 0) { prepared.measured_resize_target_provenance = .recovering; } } if (row_delta == 0) { if (origin_projection) |projection| { const origin = projection.origin; trace_source_cols = origin.source_cols; trace_current_cols = projection.current_cols; trace_source_visual_offset = origin.source_visual_offset; trace_endpoint_status = if (origin.endpoint != null) "exact" else "unavailable"; if (self.layout.cols == origin.source_cols and self.layout.cols > projection.current_cols) { prepared.measured_history_origin_consumed = true; const total_visual_rows = prepared.sourceTotalVisualRows(); if (origin.source_visual_offset <= total_visual_rows) { trace_origin_action = "restore"; debug_trace.logf( "resize", "viewport_history_origin row_delta={d} endpoint={s} action={s} source_cols={d} current_cols={d} target_cols={d} source_visual_offset={d} committed_offset={d}", .{ row_delta, trace_endpoint_status, trace_origin_action, trace_source_cols, trace_current_cols, self.layout.cols, trace_source_visual_offset, committed_offset, }, ); return origin.source_visual_offset; } trace_origin_action = "rebase"; debug_trace.logf( "resize", "viewport_history_origin row_delta={d} endpoint={s} action={s} source_cols={d} current_cols={d} target_cols={d} source_visual_offset={d} total_visual_rows={d}", .{ row_delta, trace_endpoint_status, trace_origin_action, trace_source_cols, trace_current_cols, self.layout.cols, trace_source_visual_offset, total_visual_rows, }, ); return null; } } } if (recovery_accepted_visual_offset != null and prepared.measured_resize_target_provenance != .recovering) { return null; } const measured_target = exact_measured_target orelse offsetBySignedRows(committed_offset, row_delta); debug_trace.logf( "resize", "viewport_history_origin row_delta={d} endpoint={s} action={s} source_cols={d} current_cols={d} target_cols={d} source_visual_offset={d} committed_offset={d}", .{ row_delta, trace_endpoint_status, trace_origin_action, trace_source_cols, trace_current_cols, self.layout.cols, trace_source_visual_offset, committed_offset, }, ); return preserveWelcomeReplayBoundary( measured_target, row_delta, source.welcome_boundary, prepared.sourceVisualRows(), );}
fn prepareTranscriptSurfacePaintInternal( self: anytype, alloc: Allocator, metrics: *Metrics, bottom_reserved_rows: u16, solved_area: ?render_engine.frame_layout.FrameRect, source: *const TranscriptPreparationSource, commit_runtime_state: bool, allow_projection_rebase: bool, resize_history_policy: ResizeHistoryPolicy, forced_visual_offset: ?u32,) !PreparedTranscriptSurfacePaint { if (commit_runtime_state) try self.ensurePaintReservation(alloc);
const tracked_entry_id = source.tracked_entry_id; const tracked_entry_start_line = source.tracked_entry_start_line; const transcript_bytes = source.bytes; const effective_replaceable_last_line = source.replaceable_last_line; const effective_replaceable_start = source.replaceable_start; const effective_replaceable_row = source.replaceable_row;
const initial_top = if (solved_area) |area| area.top else @max(if (self.viewport_top_row < 1) 1 else self.viewport_top_row, self.owned_top_row); const max_reserved = if (self.layout.content_bottom > 1) self.layout.content_bottom - 1 else 0; const reserved_bottom_rows = if (solved_area != null) 0 else @min(bottom_reserved_rows, max_reserved); const reserved_bottom: u16 = if (solved_area) |area| @min(area.bottom, self.layout.rows) else self.layout.content_bottom - reserved_bottom_rows; const initial_bottom: u16 = reserved_bottom; const empty_selection: ViewportSelection = .{ .top_row = initial_top, .bottom_row = if (initial_bottom >= initial_top) initial_bottom else initial_top, .start_line = 0, .partial_skip_rows = 0, .line_count = 0, .last_visible_row = 0, .last_visible_row_blank = false, .tail_kind = null, .replaceable_start_row = effective_replaceable_row, .split_active = false, .split_prefix_lines = 0, .split_suffix_start_line = 0, };
if (transcript_bytes.len == 0) { if (commit_runtime_state) self.last_paint_bottom_reserved_rows = 0; if (commit_runtime_state and solved_area == null) { self.last_visible_transcript_last_row = 0; self.last_visible_transcript_last_row_blank = false; self.last_visible_transcript_tail_kind = null; self.last_viewport_selection = null; } return .{ .selection = empty_selection, .projection_area = if (initial_bottom >= initial_top) .{ .top = initial_top, .bottom = initial_bottom } else .empty(), .bottom_reserved_rows = reserved_bottom_rows, .tracked_entry_id = tracked_entry_id, .replaceable_last_line = effective_replaceable_last_line, .replaceable_start = effective_replaceable_start, .replaceable_row = effective_replaceable_row, .cursor = .{ .cursor_row = empty_selection.top_row, .cursor_col = 1, .replaceable_row = effective_replaceable_row, }, }; }
const paint_entry_len = if (commit_runtime_state) self.beginPaint() else 0; defer if (commit_runtime_state) self.endPaint(paint_entry_len);
var prepared = PreparedTranscriptSurfacePaint{ .bytes = @constCast(transcript_bytes), .owns_bytes = false, .selection = empty_selection, .bottom_reserved_rows = reserved_bottom_rows, .tracked_entry_id = tracked_entry_id, .replaceable_last_line = effective_replaceable_last_line, .replaceable_start = effective_replaceable_start, .replaceable_row = effective_replaceable_row, .cursor = .{ .cursor_row = empty_selection.top_row, .cursor_col = 1, .replaceable_row = effective_replaceable_row, }, }; errdefer prepared.deinit(alloc); prepared.finality_candidates = try source.finality.clone(alloc); const transcript_buf = TranscriptBuffer{ .bytes = prepared.bytes };
debug_trace.logf("render", "viewport_enter layout={d}x{d} content_bottom={d} viewport_top={d} cursor={d},{d} replaceable={{last={s} row={d} start={d}}} transcript_bytes={d} entries={d}", .{ self.layout.cols, self.layout.rows, self.layout.content_bottom, self.viewport_top_row, self.cursor_row, self.cursor_col, if (effective_replaceable_last_line) "true" else "false", effective_replaceable_row, effective_replaceable_start, prepared.bytes.len, self.entries.items.len });
var owned_hard_lines: ?HardLineStarts = null; const hard_lines = if (source.hard_line_starts.len > 0) HardLineStarts{ .starts = source.hard_line_starts, .ends_with_newline = source.hard_lines_end_with_newline, } else blk: { owned_hard_lines = try buildHardLineStarts(alloc, transcript_buf.bytes); break :blk owned_hard_lines.?; }; defer if (owned_hard_lines) |lines| lines.deinit(alloc);
var top_row: u16 = initial_top; if (solved_area == null) { if (commit_runtime_state) { self.last_paint_bottom_reserved_rows = reserved_bottom_rows; } } const bottom_row: u16 = initial_bottom; if (bottom_row < top_row) { if (commit_runtime_state) self.pending_scroll_compact = true; debug_trace.logf("render", "viewport_skip bottom={d} < top={d} (band collapsed)", .{ bottom_row, top_row }); if (solved_area == null) { try compactAfterCollapsedResize(self, metrics); top_row = @max(if (self.viewport_top_row < 1) 1 else self.viewport_top_row, self.owned_top_row); } if (bottom_row < top_row) return prepared; } if (solved_area == null) { try compactAfterCollapsedResize(self, metrics); top_row = @max(if (self.viewport_top_row < 1) 1 else self.viewport_top_row, self.owned_top_row); } prepared.projection_area = .{ .top = top_row, .bottom = bottom_row };
const transcript_line_count = hard_lines.len(); // This count sizes the appendAssumeCapacity walk below, which expands // folded blocks only through a source snapshot's summary indices. const expand_folded_summaries = !commit_runtime_state or source.recorded_entries_authoritative; const total_lines = if (expand_folded_summaries) source_preparation.visibleTranscriptLineCountFromSource( self, transcript_line_count, source.folded_summary_indices, ) else transcript_line_count; var visible_rows: u16 = bottom_row - top_row + 1;
const capture_line_provenance = source.line_provenance.len > 0; var repl_line_idx: usize = total_lines; var tracked_visible_line: ?usize = null; const precomputed_plain_lines = !effective_replaceable_last_line and tracked_entry_start_line == null and total_lines == transcript_line_count and source.transcript_visible_lines.len == transcript_line_count and source.transcript_line_visual_rows.len == transcript_line_count and (!capture_line_provenance or source.line_provenance.len == transcript_line_count); if (precomputed_plain_lines) { prepared.source_visible_lines = source.transcript_visible_lines; prepared.source_line_visual_rows = source.transcript_line_visual_rows; prepared.source_line_provenance = source.line_provenance; prepared.source_visual_row_offsets = source.transcript_visual_row_offsets; } else { try prepared.visible_lines.ensureTotalCapacity(alloc, total_lines); try prepared.line_visual_rows.ensureTotalCapacity(alloc, total_lines); if (capture_line_provenance) { try prepared.line_provenance.ensureTotalCapacity(alloc, total_lines); } var transcript_index: usize = 0; while (transcript_index < transcript_line_count) : (transcript_index += 1) { if (tracked_entry_start_line == transcript_index) { tracked_visible_line = prepared.visible_lines.items.len; } const matched_block = if (expand_folded_summaries) source_preparation.foldedBlockAtTranscriptLine( self, source.folded_summary_indices, transcript_index, ) else null;
if (matched_block) |block| { const effective_cols: u16 = if (self.layout.cols > 2) self.layout.cols - 2 else self.layout.cols; for (block.lines.items) |line| { const trimmed = stripTrailingNewline(line.text); prepared.visible_lines.appendAssumeCapacity(.{ .folded_line = .{ .text = line.text, .stream = line.stream } }); prepared.line_visual_rows.appendAssumeCapacity(visualRowsForLine(trimmed, effective_cols)); if (capture_line_provenance) { prepared.line_provenance.appendAssumeCapacity(.{ .folded_command_output = .{ .entry_id = block.summary_entry_id, .stream = line.stream, }, }); } } continue; }
const ref = hardLineRefAt(hard_lines, transcript_buf.bytes.len, transcript_index); const transcript_ref = TranscriptRef{ .ref = ref }; const text = transcript_ref.resolve(transcript_buf); const vr = if (source.transcript_line_visual_rows.len == transcript_line_count) source.transcript_line_visual_rows[transcript_index] else visualRowsForLine(text, self.layout.cols); const visible_index = prepared.visible_lines.items.len; prepared.visible_lines.appendAssumeCapacity(visibleFromTranscript(ref)); prepared.line_visual_rows.appendAssumeCapacity(vr); if (capture_line_provenance) { prepared.line_provenance.appendAssumeCapacity( if (transcript_index < source.line_provenance.len) source.line_provenance[transcript_index] else .unattributed, ); }
if (effective_replaceable_last_line and hard_lines.starts[transcript_index] <= effective_replaceable_start) { repl_line_idx = visible_index; } } }
const selection_result = buildViewportSelection(.{ .top_row = top_row, .bottom_row = bottom_row, .line_visual_rows = prepared.sourceVisualRows(), .visible_rows = visible_rows, .welcome_cut_line = source.welcome_cut_line, .replaceable_line_index = repl_line_idx, .tail_kind = source.tail_kind, }); var viewport_selection_snapshot = selection_result.selection; var rows_budget = selection_result.rows_budget_unused; var welcome_decision = selection_result.welcome_decision; var measured_start_flow_offset: ?usize = null; const resize_history_target: ?u32 = if (resize_history_policy == .apply) try resolveResizeHistoryTarget( self, alloc, &prepared, transcript_bytes, source, &measured_start_flow_offset, ) else null; if (resize_history_target) |target_offset| { if (applyResizeHistoryOffset( &viewport_selection_snapshot, prepared.sourceVisualRows(), target_offset, )) { rows_budget = unusedRowsForSelection( prepared.sourceVisualRows(), viewport_selection_snapshot, visible_rows, ); welcome_decision = .none; const row_delta = self.resize_history_row_delta.?; debug_trace.logf( "resize", "viewport_history_prefix_omitted target_offset={d} row_delta={d} start_line={d} partial_skip_rows={d}", .{ target_offset, row_delta, viewport_selection_snapshot.start_line, viewport_selection_snapshot.partial_skip_rows, }, ); } } if (measured_start_flow_offset) |flow_offset| { prepared.projection_start_flow_offset = flow_offset; try replacePreparedProjectionResumeBytesForFlowOffset( alloc, &prepared, self.layout.cols, flow_offset, ); } if (comptime @hasDecl(@TypeOf(self.*), "stableTranscriptProjectionForFlow")) { if (!viewport_selection_snapshot.split_active and self.resize_history_row_delta == null and self.committed_frame_layout.terminal_cols == self.layout.cols) { if (self.stableTranscriptProjectionForFlow(transcript_bytes)) |stable| { const exact_history_reflow = stable.flow_unchanged and stable.measured_history_origin != null and stable.visual_offset == stable.history_visual_offset; const settled_history_reflow = stable.flow_unchanged and stable.visual_offset == stable.history_visual_offset and source.tail_kind != .assistant_turn; const resize_anchored_cancel_growth = !stable.flow_unchanged and self.resize_bottom_anchor_pending and source.tail_kind == .cancel_notice; const repaint_required = self.render_requests.attemptHasInvalidations(); const invalidation_allows_preservation = !repaint_required or settled_history_reflow; const next_offset = prepared.sourceVisualOffsetFor(viewport_selection_snapshot); if (allow_projection_rebase and !stable.flow_unchanged and self.committed_frame_layout.terminal_rows == self.layout.rows and invalidation_allows_preservation and next_offset < stable.history_visual_offset) { setSelectionVisualOffset( &viewport_selection_snapshot, prepared.sourceVisualRows(), stable.history_visual_offset, ); rows_budget = unusedRowsForSelection( prepared.sourceVisualRows(), viewport_selection_snapshot, visible_rows, ); welcome_decision = .none; debug_trace.logf( "scroll", "transcript_projection_history_floor source_offset={d} natural_offset={d}", .{ stable.history_visual_offset, next_offset }, ); } if (resize_anchored_cancel_growth and rows_budget > 0 and rows_budget < visible_rows) { top_row += rows_budget; visible_rows -= rows_budget; prepared.projection_area.top = top_row; viewport_selection_snapshot.top_row = top_row; } if (invalidation_allows_preservation and (!allow_projection_rebase or exact_history_reflow or settled_history_reflow)) { const committed = stable.selection; if (!committed.split_active and committed.start_line < viewport_selection_snapshot.line_count and next_offset < stable.visual_offset) { viewport_selection_snapshot.start_line = committed.start_line; viewport_selection_snapshot.partial_skip_rows = committed.partial_skip_rows; const stable_occupied_rows = stable.occupied_last_row -| stable.selection.top_row +| 1; const align_stable_cancel_to_bottom = (repaint_required or stable.occupied_last_row == bottom_row) and settled_history_reflow and source.tail_kind == .cancel_notice and stable_occupied_rows > 0 and stable_occupied_rows < visible_rows; if (align_stable_cancel_to_bottom) { top_row = bottom_row - stable_occupied_rows + 1; visible_rows = stable_occupied_rows; prepared.projection_area.top = top_row; viewport_selection_snapshot.top_row = top_row; } debug_trace.logf( "scroll", "transcript_projection_preserved source_offset={d} natural_offset={d} start_line={d} partial_skip_rows={d}", .{ stable.visual_offset, next_offset, committed.start_line, committed.partial_skip_rows, }, ); } } } } } if (comptime @hasField(@TypeOf(self.*), "tail_viewport_request")) { if (self.tail_viewport_request) |request| { const resolution = viewport_selection.resolveTailOffset(.{ .total_rows = prepared.sourceTotalVisualRows(), .visible_rows = visible_rows, .policy = request, }); setSelectionVisualOffset( &viewport_selection_snapshot, prepared.sourceVisualRows(), resolution.start_visual_row, ); rows_budget = unusedRowsForSelection( prepared.sourceVisualRows(), viewport_selection_snapshot, visible_rows, ); welcome_decision = .none; self.tail_viewport_resolution = resolution; } } if (forced_visual_offset) |visual_offset| { const remaining_visual_rows = projectionRemainingVisualRows( &prepared, visual_offset, ) orelse return error.InvalidTranscriptTransition; const projection_rows = @min( remaining_visual_rows, @as(u32, visible_rows), ); if (projection_rows == 0) return error.InvalidTranscriptTransition; const visual_end = visual_offset + projection_rows; const start = sourceStartPosition(&prepared, visual_offset) orelse return error.InvalidTranscriptTransition; const boundary = preparedProjectionBoundary( &prepared, self.layout.cols, visual_end, ) orelse return error.InvalidTranscriptTransition; try replacePreparedProjectionResumeBytes( alloc, &prepared, self.layout.cols, visual_offset, ); prepared.projection_visual_rows = @intCast(projection_rows); prepared.projection_ends_with_newline = boundary.line_terminated; viewport_selection_snapshot.start_line = start.line; viewport_selection_snapshot.partial_skip_rows = start.intra_line_rows; viewport_selection_snapshot.line_count = prepared.sourceVisibleLines().len; viewport_selection_snapshot.last_visible_row = 0; viewport_selection_snapshot.last_visible_row_blank = false; viewport_selection_snapshot.replaceable_start_row = top_row; rows_budget = visible_rows - @as(u16, @intCast(projection_rows)); welcome_decision = .none; } const start_line = viewport_selection_snapshot.start_line; const partial_skip_rows = viewport_selection_snapshot.partial_skip_rows; const render_total_lines = viewport_selection_snapshot.line_count;
if (viewport_selection_snapshot.split_active) { try prepared.selected_visible_lines.ensureTotalCapacity(alloc, render_total_lines); try prepared.selected_line_visual_rows.ensureTotalCapacity(alloc, render_total_lines); if (capture_line_provenance) { try prepared.selected_line_provenance.ensureTotalCapacity(alloc, render_total_lines); } var selected_index: usize = 0; while (selected_index < render_total_lines) : (selected_index += 1) { const source_index = selectedSourceLineIndex(viewport_selection_snapshot, selected_index).?; prepared.selected_visible_lines.appendAssumeCapacity(prepared.sourceVisibleLines()[source_index]); prepared.selected_line_visual_rows.appendAssumeCapacity(prepared.sourceVisualRows()[source_index]); if (capture_line_provenance) { prepared.selected_line_provenance.appendAssumeCapacity( prepared.lineProvenance()[source_index], ); } } prepared.use_selected = true; }
switch (welcome_decision) { .none => {}, .pinned_welcome => debug_trace.logf( "render", "viewport_pin_welcome cut_line={d} suffix_start={d} total_lines={d} selected_lines={d} rows_budget_unused={d}", .{ viewport_selection_snapshot.split_prefix_lines, viewport_selection_snapshot.split_suffix_start_line, selection_result.original_total_lines, render_total_lines, rows_budget, }, ), .dropped_partial_welcome => debug_trace.logf( "render", "viewport_drop_partial_welcome start_line={d} cut_line={d} total_lines={d}", .{ selection_result.natural_start_line, selection_result.welcome_cut_line.?, selection_result.original_total_lines, }, ), }
debug_trace.logf("render", "viewport_select start_line={d} total_lines={d} visible_rows={d} rows_budget_unused={d} partial_skip_rows={d}", .{ start_line, render_total_lines, visible_rows, rows_budget, partial_skip_rows }); const trace_transcript_frame = !self.paint_trace.transcript_initialized or self.paint_trace.transcript_top != top_row or self.paint_trace.transcript_bottom != bottom_row or self.paint_trace.transcript_start_line != start_line or self.paint_trace.transcript_total_lines != render_total_lines or self.paint_trace.transcript_visible_rows != visible_rows or self.paint_trace.transcript_partial_skip_rows != partial_skip_rows or self.paint_trace.transcript_entries != self.entries.items.len; prepared.trace_transcript_frame = trace_transcript_frame; prepared.trace_visible_rows = visible_rows; if (commit_runtime_state) { self.paint_trace.transcript_initialized = true; self.paint_trace.transcript_top = top_row; self.paint_trace.transcript_bottom = bottom_row; self.paint_trace.transcript_start_line = start_line; self.paint_trace.transcript_total_lines = render_total_lines; self.paint_trace.transcript_visible_rows = visible_rows; self.paint_trace.transcript_partial_skip_rows = partial_skip_rows; self.paint_trace.transcript_entries = self.entries.items.len; self.paint_trace.transcript_log_frame = trace_transcript_frame; } if (trace_transcript_frame) { debug_trace.logf( "paint", "transcript_select top={d} bottom={d} start_line={d} total_lines={d} visible_rows={d} partial_skip_rows={d} rows_budget_unused={d} hard_lines={d} entries={d} bytes={d}", .{ top_row, bottom_row, start_line, render_total_lines, visible_rows, partial_skip_rows, rows_budget, transcript_line_count, self.entries.items.len, transcript_buf.bytes.len, }, ); }
if (commit_runtime_state and solved_area == null) { self.last_visible_transcript_top_row = top_row; self.last_visible_transcript_start_line = start_line; self.last_visible_transcript_partial_skip_rows = partial_skip_rows; self.last_visible_transcript_split_active = viewport_selection_snapshot.split_active; self.last_visible_transcript_split_prefix_lines = viewport_selection_snapshot.split_prefix_lines; self.last_visible_transcript_split_suffix_start_line = viewport_selection_snapshot.split_suffix_start_line; self.last_visible_transcript_tail_kind = viewport_selection_snapshot.tail_kind; self.resetCursorTracking(); self.cursor_row = top_row; }
prepared.selection = viewport_selection_snapshot; prepared.total_lines = render_total_lines; prepared.transcript_ends_with_newline = hard_lines.ends_with_newline; const render_context = .{ .layout = self.layout, .replaceable_last_line = effective_replaceable_last_line, .replaceable_start = effective_replaceable_start, .replaceable_row = effective_replaceable_row, }; if (commit_runtime_state) { if (solved_area == null) { try refreshPreparedTranscriptState(self, alloc, &prepared, true); } else { try refreshPreparedTranscriptState(self, alloc, &prepared, false); } } else { try refreshPreparedTranscriptState(&render_context, alloc, &prepared, false); } if (tracked_visible_line) |line_index| { prepared.tracked_entry_row = rowForVisibleLineIndex( prepared.selection, prepared.sourceVisualRows(), line_index, prepared.selection.bottom_row, ); } return prepared;}
fn refreshPreparedTranscriptState( self: anytype, alloc: Allocator, prepared: *PreparedTranscriptSurfacePaint, comptime commit_runtime_state: bool,) !void { if (prepared.total_lines == 0) return;
const viewport = prepared.selection; var rendered_rows = try renderTranscriptRows( self, alloc, prepared.batch(), prepared.visualRows(), prepared.total_lines, viewport.start_line, viewport.partial_skip_rows, viewport.top_row, viewport.bottom_row, prepared.projection_visual_rows, prepared.projection_resume_bytes, prepared.projection_start_flow_offset, ); defer rendered_rows.deinit(alloc);
applyRenderedRowsToSelection(&prepared.selection, &rendered_rows); try updatePreparedRowProvenance(alloc, prepared, &rendered_rows); prepared.cursor = calculateViewportCursor( self.layout, viewport.bottom_row, prepared.total_lines, viewport.start_line, rendered_rows.replaceable_line_index, rendered_rows.replaceable_start_row, rendered_rows.last_visible_line, rendered_rows.last_visible_row, rendered_rows.last_line_is_folded, viewport.top_row, prepared.projection_ends_with_newline orelse prepared.transcript_ends_with_newline, self.replaceable_last_line, self.replaceable_row, );
if (commit_runtime_state) { self.last_visible_transcript_last_row = rendered_rows.last_visible_row; self.last_visible_transcript_last_row_blank = rendered_rows.last_visible_row_blank; self.last_viewport_selection = prepared.selection; self.cursor_row = prepared.cursor.cursor_row; self.cursor_col = prepared.cursor.cursor_col; if (self.replaceable_last_line) self.replaceable_row = prepared.cursor.replaceable_row; } debug_trace.logf( "render", "viewport_prepared cursor={d},{d} last_visible_row={d} replaceable_row={d} reserved_rows={d}", .{ prepared.cursor.cursor_row, prepared.cursor.cursor_col, rendered_rows.last_visible_row, prepared.cursor.replaceable_row, prepared.bottom_reserved_rows }, );}
fn updatePreparedRowProvenance( alloc: Allocator, prepared: *PreparedTranscriptSurfacePaint, rendered_rows: *const RenderedTranscriptRows,) !void { prepared.row_provenance.clearRetainingCapacity(); const line_provenance = prepared.lineProvenance(); if (line_provenance.len == 0) return;
for (rendered_rows.rows.items) |rendered| { const source = if (rendered.line_index < line_provenance.len) line_provenance[rendered.line_index] else .unattributed; var offset: u16 = 0; while (offset < rendered.rows_painted) : (offset += 1) { try prepared.row_provenance.append(alloc, .{ .row = rendered.row + offset, .source = source, }); } }}
pub fn reprojectPreparedTranscriptForVisualOffset( alloc: Allocator, layout: types.Layout, prepared: *PreparedTranscriptSurfacePaint, area: render_engine.frame_layout.FrameRect, visual_offset: u32,) !void { if (prepared.selection.split_active or prepared.use_selected or area.isEmpty() or area.top == 0 or area.bottom > layout.content_bottom) { return error.InvalidTranscriptTransition; }
prepared.projection_visual_rows = null; prepared.projection_ends_with_newline = null; prepared.projection_cursor = null; try replacePreparedProjectionResumeBytes( alloc, prepared, layout.cols, visual_offset, ); const remaining_visual_rows = projectionRemainingVisualRows(prepared, visual_offset) orelse return error.InvalidTranscriptTransition; if (remaining_visual_rows > area.height()) { return error.InvalidTranscriptTransition; }
const start = sourceStartPosition(prepared, visual_offset) orelse return error.InvalidTranscriptTransition;
prepared.selection.top_row = area.top; prepared.selection.bottom_row = area.bottom; prepared.selection.start_line = start.line; prepared.selection.partial_skip_rows = start.intra_line_rows; prepared.selection.last_visible_row = 0; prepared.selection.last_visible_row_blank = false; prepared.selection.replaceable_start_row = area.top;
if (prepared.total_lines == 0) { if (visual_offset != 0) return error.InvalidTranscriptTransition; prepared.cursor = .{ .cursor_row = area.top, .cursor_col = 1, .replaceable_row = prepared.replaceable_row, }; return; }
const render_context = .{ .layout = layout, .replaceable_last_line = prepared.replaceable_last_line, .replaceable_start = prepared.replaceable_start, .replaceable_row = prepared.replaceable_row, }; try refreshPreparedTranscriptState( &render_context, alloc, prepared, false, ); if (prepared.selection.last_visible_row > area.bottom) { return error.InvalidTranscriptTransition; }}
pub const PreparedTranscriptProjection = struct { flow_end: ?usize,};
pub fn preparedTranscriptStagedFlowEndExactness( prepared: *const PreparedTranscriptSurfacePaint, cols: u16, area: render_engine.frame_layout.FrameRect, visual_offset: u32,) ?bool { if (prepared.selection.split_active or prepared.use_selected or area.isEmpty() or area.top == 0) { return null; } const remaining_visual_rows = projectionRemainingVisualRows(prepared, visual_offset) orelse return null; const projection_rows = @min( remaining_visual_rows, @as(u32, area.height()), ); if (projection_rows == 0) return null; const boundary = preparedProjectionBoundary( prepared, cols, visual_offset + projection_rows, ) orelse return null; return boundary.flow_end != null;}
pub fn preparedTranscriptFlowOffsetForVisualOffset( prepared: *const PreparedTranscriptSurfacePaint, cols: u16, visual_offset: u32,) ?usize { const boundary = preparedProjectionBoundary( prepared, cols, visual_offset, ) orelse return null; return boundary.flow_end;}
pub fn stagePreparedTranscriptForVisualOffset( alloc: Allocator, layout: types.Layout, prepared: *PreparedTranscriptSurfacePaint, area: render_engine.frame_layout.FrameRect, visual_offset: u32,) !PreparedTranscriptProjection { if (prepared.selection.split_active or prepared.use_selected or area.isEmpty() or area.top == 0 or area.bottom > layout.content_bottom) { return error.InvalidTranscriptTransition; }
const remaining_visual_rows = projectionRemainingVisualRows(prepared, visual_offset) orelse return error.InvalidTranscriptTransition; const projection_rows = @min( remaining_visual_rows, @as(u32, area.height()), ); if (projection_rows == 0) return error.InvalidTranscriptTransition; const visual_end = visual_offset + projection_rows;
const start = sourceStartPosition(prepared, visual_offset) orelse return error.InvalidTranscriptTransition; if (start.line == prepared.sourceVisualRows().len) { return error.InvalidTranscriptTransition; }
const boundary = preparedProjectionBoundary(prepared, layout.cols, visual_end) orelse return error.InvalidTranscriptTransition; try replacePreparedProjectionResumeBytes( alloc, prepared, layout.cols, visual_offset, ); prepared.projection_area = area; prepared.projection_visual_rows = @intCast(projection_rows); prepared.projection_ends_with_newline = boundary.line_terminated; prepared.projection_cursor = null; prepared.selection.top_row = area.top; prepared.selection.bottom_row = area.bottom; prepared.selection.start_line = start.line; prepared.selection.partial_skip_rows = start.intra_line_rows; prepared.selection.line_count = prepared.sourceVisibleLines().len; prepared.selection.last_visible_row = 0; prepared.selection.last_visible_row_blank = false; prepared.selection.replaceable_start_row = area.top;
const render_context = .{ .layout = layout, .replaceable_last_line = prepared.replaceable_last_line, .replaceable_start = prepared.replaceable_start, .replaceable_row = prepared.replaceable_row, }; try refreshPreparedTranscriptState( &render_context, alloc, prepared, false, ); if (prepared.selection.last_visible_row > area.bottom) { return error.InvalidTranscriptTransition; } if (remaining_visual_rows > projection_rows and prepared.selection.last_visible_row < layout.rows) { prepared.cursor = .{ .cursor_row = prepared.selection.last_visible_row + 1, .cursor_col = 1, .replaceable_row = prepared.cursor.replaceable_row, }; prepared.projection_cursor = prepared.cursor; } return .{ .flow_end = boundary.flow_end, };}
const PreparedProjectionBoundary = struct { flow_end: ?usize, line_terminated: bool,};
const PreparedControlBoundary = struct { resume_bytes: []u8 = &.{}, steady_bytes: []u8 = &.{}, cursor_col: u16 = 1, pending_wrap: bool = false,
fn deinit(self: *PreparedControlBoundary, alloc: Allocator) void { if (self.resume_bytes.len > 0) alloc.free(self.resume_bytes); if (self.steady_bytes.len > 0) alloc.free(self.steady_bytes); }};
fn prepareControlBoundary( alloc: Allocator, bytes: []const u8, cols: u16, raw_offset: usize,) !PreparedControlBoundary { if (raw_offset > bytes.len) return error.InvalidTranscriptTransition;
var state = try vt_emulator.Grid.init(alloc, cols, 1); defer state.deinit(); state.defer_sync_updates = false; try state.feed(bytes[0..raw_offset]); if (!state.atControlSequenceBoundary()) { return error.InvalidTranscriptTransition; }
return captureControlBoundary(alloc, &state);}
fn captureControlBoundary( alloc: Allocator, state: *const vt_emulator.Grid,) !PreparedControlBoundary { var resume_writer: std.Io.Writer.Allocating = .init(alloc); defer resume_writer.deinit(); try state.writePresentationResume(&resume_writer.writer);
var steady_writer: std.Io.Writer.Allocating = .init(alloc); defer steady_writer.deinit(); try state.writePresentationSteady(&steady_writer.writer);
var result = PreparedControlBoundary{ .cursor_col = state.cursor_col, .pending_wrap = state.pending_wrap, }; errdefer result.deinit(alloc); if (resume_writer.written().len > 0) { result.resume_bytes = try resume_writer.toOwnedSlice(); } if (steady_writer.written().len > 0) { result.steady_bytes = try steady_writer.toOwnedSlice(); } return result;}
pub const TranscriptDocumentControlCursor = struct { state: vt_emulator.Grid, bytes: []const u8, raw_offset: usize = 0,
pub fn init( alloc: Allocator, bytes: []const u8, cols: u16, ) !TranscriptDocumentControlCursor { var state = try vt_emulator.Grid.init(alloc, cols, 1); state.defer_sync_updates = false; return .{ .state = state, .bytes = bytes, }; }
pub fn deinit(self: *TranscriptDocumentControlCursor) void { self.state.deinit(); self.* = undefined; }
pub fn prepareAppendBytes( self: *TranscriptDocumentControlCursor, alloc: Allocator, raw_start: usize, raw_end: usize, close_endpoint: bool, ) ![]u8 { if (raw_start < self.raw_offset or raw_start > raw_end or raw_end > self.bytes.len) { return error.InvalidTranscriptTransition; }
try self.state.feed(self.bytes[self.raw_offset..raw_start]); if (!self.state.atControlSequenceBoundary()) { return error.InvalidTranscriptTransition; } var start = try captureControlBoundary(alloc, &self.state); defer start.deinit(alloc);
try self.state.feed(self.bytes[raw_start..raw_end]); self.raw_offset = raw_end; if (!self.state.atControlSequenceBoundary()) { return error.InvalidTranscriptTransition; } var end = if (close_endpoint) try captureControlBoundary(alloc, &self.state) else PreparedControlBoundary{}; defer end.deinit(alloc);
return assembleTranscriptDocumentAppendBytes( alloc, self.bytes[raw_start..raw_end], start, end, ); }};
fn assembleTranscriptDocumentAppendBytes( alloc: Allocator, raw_bytes: []const u8, start: PreparedControlBoundary, end: PreparedControlBoundary,) ![]u8 { if (start.resume_bytes.len == 0 and end.steady_bytes.len == 0 and std.mem.findScalar(u8, raw_bytes, '\n') == null) { return &.{}; }
const parts = [_][]const u8{ start.resume_bytes, raw_bytes, end.steady_bytes, }; var extra_carriage_returns: usize = 0; var previous_was_carriage_return = false; for (parts) |part| { for (part) |byte| { if (byte == '\n' and !previous_was_carriage_return) { extra_carriage_returns += 1; } previous_was_carriage_return = byte == '\r'; } } const result = try alloc.alloc( u8, start.resume_bytes.len + raw_bytes.len + end.steady_bytes.len + extra_carriage_returns, ); var offset: usize = 0; previous_was_carriage_return = false; for (parts) |part| { for (part) |byte| { if (byte == '\n' and !previous_was_carriage_return) { result[offset] = '\r'; offset += 1; } result[offset] = byte; offset += 1; previous_was_carriage_return = byte == '\r'; } } return result;}
fn replacePreparedProjectionResumeBytes( alloc: Allocator, prepared: *PreparedTranscriptSurfacePaint, cols: u16, visual_offset: u32,) !void { const boundary = preparedProjectionBoundary(prepared, cols, visual_offset) orelse return error.InvalidTranscriptTransition; try replacePreparedProjectionResumeBytesForFlowOffset( alloc, prepared, cols, boundary.flow_end, );}
fn replacePreparedProjectionResumeBytesForFlowOffset( alloc: Allocator, prepared: *PreparedTranscriptSurfacePaint, cols: u16, flow_offset: ?usize,) !void { if (flow_offset) |offset| { if (prepared.projection_resume_flow_offset) |base_offset| { if (offset >= base_offset) { if (offset == base_offset) return; var advanced = try prepareResumeDocumentAppend( alloc, prepared.bytes, cols, base_offset, offset, false, .{ .resume_bytes = prepared.projection_resume_bytes, .cursor_col = prepared.projection_resume_cursor_col, .pending_wrap = prepared.projection_resume_pending_wrap, }, false, ); defer advanced.deinit(alloc); if (prepared.projection_resume_bytes.len > 0) { alloc.free(prepared.projection_resume_bytes); } prepared.projection_resume_bytes = advanced.endpoint_resume_bytes; advanced.endpoint_resume_bytes = &.{}; prepared.projection_resume_flow_offset = offset; prepared.projection_resume_cursor_col = advanced.endpoint_cursor_col; prepared.projection_resume_pending_wrap = advanced.endpoint_pending_wrap; return; } } }
var control = if (flow_offset) |offset| try prepareControlBoundary(alloc, prepared.bytes, cols, offset) else PreparedControlBoundary{}; defer control.deinit(alloc);
const resume_bytes = control.resume_bytes; control.resume_bytes = &.{}; if (prepared.projection_resume_bytes.len > 0) { alloc.free(prepared.projection_resume_bytes); } prepared.projection_resume_bytes = resume_bytes; prepared.projection_resume_flow_offset = null; prepared.projection_resume_cursor_col = 1; prepared.projection_resume_pending_wrap = false;}
pub const PresentationBoundary = struct { resume_bytes: []const u8 = &.{}, cursor_col: u16 = 1, pending_wrap: bool = false,};
pub fn seedPreparedPresentationBoundary( alloc: Allocator, prepared: *PreparedTranscriptSurfacePaint, flow_offset: usize, boundary: PresentationBoundary,) !void { const owned = try alloc.dupe(u8, boundary.resume_bytes); if (prepared.projection_resume_bytes.len > 0) { alloc.free(prepared.projection_resume_bytes); } prepared.projection_resume_bytes = owned; prepared.projection_resume_flow_offset = flow_offset; prepared.projection_resume_cursor_col = boundary.cursor_col; prepared.projection_resume_pending_wrap = boundary.pending_wrap;}
/// Returns allocator-owned terminal-wire bytes when boundary state or LF is/// present. An empty slice means the raw source slice is LF-free and wire-safe.pub fn prepareTranscriptDocumentAppendBytes( alloc: Allocator, bytes: []const u8, cols: u16, raw_start: usize, raw_end: usize, close_endpoint: bool,) ![]u8 { if (raw_start > raw_end or raw_end > bytes.len) { return error.InvalidTranscriptTransition; }
var append = try prepareTranscriptDocumentAppend(alloc, bytes, cols, raw_start, raw_end, close_endpoint); defer append.deinit(alloc); const result = append.bytes; append.bytes = &.{}; return result;}
/// Owns wire bytes and the source boundary needed for a cursor-addressed append.pub fn prepareTranscriptDocumentAppend( alloc: Allocator, bytes: []const u8, cols: u16, raw_start: usize, raw_end: usize, close_endpoint: bool,) !PreparedResumeDocumentAppend { var start = try prepareControlBoundary(alloc, bytes, cols, raw_start); defer start.deinit(alloc); return prepareResumeDocumentAppend( alloc, bytes, cols, raw_start, raw_end, close_endpoint, .{ .resume_bytes = start.resume_bytes, .cursor_col = start.cursor_col, .pending_wrap = start.pending_wrap, }, true, );}
test "document append preparation emits terminal-ready newlines" { const cases = [_]struct { input: []const u8, expected: []const u8, }{ .{ .input = "one\ntwo\n", .expected = "one\r\ntwo\r\n" }, .{ .input = "one\r\ntwo\r", .expected = "one\r\ntwo\r" }, }; for (cases) |case| { const prepared = try prepareTranscriptDocumentAppendBytes( std.testing.allocator, case.input, 80, 0, case.input.len, false, ); defer std.testing.allocator.free(prepared); try std.testing.expectEqualStrings(case.expected, prepared); }
const prepared = try prepareTranscriptDocumentAppendBytes( std.testing.allocator, "no newline", 80, 0, "no newline".len, false, ); try std.testing.expectEqual(@as(usize, 0), prepared.len);}
test "document append preparation preserves presentation boundaries" { const open = "\x1b]8;id=fx-42;https://example.com\x1b\\"; const raw = "old\nnew"; const close = "\x1b]8;;\x1b\\"; const source = open ++ raw ++ close; const prepared = try prepareTranscriptDocumentAppendBytes( std.testing.allocator, source, 80, open.len, open.len + raw.len, true, ); defer std.testing.allocator.free(prepared); try std.testing.expectEqualStrings(open ++ "old\r\nnew" ++ close, prepared);}
pub const PreparedResumeDocumentAppend = struct { bytes: []u8 = &.{}, start_pending_wrap: bool = false, endpoint_resume_bytes: []u8 = &.{}, endpoint_pending_wrap: bool = false, endpoint_cursor_col: u16 = 1,
pub fn deinit(self: *PreparedResumeDocumentAppend, alloc: Allocator) void { if (self.bytes.len > 0) alloc.free(self.bytes); if (self.endpoint_resume_bytes.len > 0) alloc.free(self.endpoint_resume_bytes); self.* = undefined; }};
pub fn prepareResumeDocumentAppend( alloc: Allocator, bytes: []const u8, cols: u16, raw_start: usize, raw_end: usize, close_endpoint: bool, start: PresentationBoundary, materialize_bytes: bool,) !PreparedResumeDocumentAppend { if (raw_start > raw_end or raw_end > bytes.len) { return error.InvalidTranscriptTransition; }
var state = try vt_emulator.Grid.init(alloc, cols, 1); defer state.deinit(); state.defer_sync_updates = false; try state.restorePresentationBoundary( start.resume_bytes, start.cursor_col, start.pending_wrap, );
const raw_bytes = bytes[raw_start..raw_end]; try state.feed(raw_bytes); if (!state.atControlSequenceBoundary()) return error.InvalidTranscriptTransition;
var resume_writer: std.Io.Writer.Allocating = .init(alloc); defer resume_writer.deinit(); try state.writePresentationResume(&resume_writer.writer); var steady_writer: std.Io.Writer.Allocating = .init(alloc); defer steady_writer.deinit(); if (close_endpoint) try state.writePresentationSteady(&steady_writer.writer);
var result = PreparedResumeDocumentAppend{ .start_pending_wrap = start.pending_wrap, .endpoint_pending_wrap = state.pending_wrap, .endpoint_cursor_col = state.cursor_col, }; errdefer result.deinit(alloc); if (resume_writer.written().len > 0) { result.endpoint_resume_bytes = try resume_writer.toOwnedSlice(); } const steady_bytes = steady_writer.written(); const parts = [_][]const u8{ start.resume_bytes, raw_bytes, steady_bytes }; var extra_carriage_returns: usize = 0; var previous_was_carriage_return = false; for (parts) |part| { for (part) |byte| { if (byte == '\n' and !previous_was_carriage_return) { extra_carriage_returns += 1; } previous_was_carriage_return = byte == '\r'; } } if (materialize_bytes and (start.resume_bytes.len > 0 or steady_bytes.len > 0 or std.mem.findScalar(u8, raw_bytes, '\n') != null)) { result.bytes = try alloc.alloc( u8, start.resume_bytes.len + raw_bytes.len + steady_bytes.len + extra_carriage_returns, ); var offset: usize = 0; previous_was_carriage_return = false; for (parts) |part| { for (part) |byte| { if (byte == '\n' and !previous_was_carriage_return) { result.bytes[offset] = '\r'; offset += 1; } result.bytes[offset] = byte; offset += 1; previous_was_carriage_return = byte == '\r'; } } } return result;}
pub fn prepareResetReplayDocument( self: anytype, alloc: Allocator, transcript_bytes: []const u8, visible_lines: []const VisibleTranscriptLine, line_visual_rows: []const u16, transcript_ends_with_newline: bool, padding_rows: u16,) ![]u8 { if (visible_lines.len != line_visual_rows.len) { return error.InvalidTranscriptTransition; } const visual_rows = totalVisualRows(line_visual_rows); if (visual_rows == 0) return &.{};
const batch = VisibleTranscriptBatch{ .transcript = .{ .bytes = transcript_bytes }, .lines = visible_lines, }; var output: std.Io.Writer.Allocating = .init(alloc); errdefer output.deinit(); var presentation = try vt_emulator.Grid.init(alloc, self.layout.cols, 1); defer presentation.deinit();
var remaining_visual_rows = visual_rows; var start_line: usize = 0; var partial_skip_rows: u16 = 0; while (remaining_visual_rows > 0) { const render_limit: u16 = @intCast(@min( remaining_visual_rows, @as(u32, std.math.maxInt(u16)), )); var rendered = try renderTranscriptRows( self, alloc, batch, line_visual_rows, visible_lines.len, start_line, partial_skip_rows, 1, render_limit, render_limit, &.{}, null, ); defer rendered.deinit(alloc); if (rendered.painted_rows == 0) return error.InvalidTranscriptTransition;
const rendered_visual_rows: u32 = rendered.painted_rows; if (rendered_visual_rows > remaining_visual_rows) { return error.InvalidTranscriptTransition; } const final_render = rendered_visual_rows == remaining_visual_rows; for (rendered.rows.items, 0..) |row, row_index| { try output.writer.writeAll(row.bytes); try presentation.feed(row.bytes);
const line_rows = line_visual_rows[row.line_index]; const line_complete = row.partial_skip_rows + row.rows_painted == line_rows; const final_source_line = final_render and row_index + 1 == rendered.rows.items.len and row.line_index + 1 == visible_lines.len; const terminate_line = line_complete and (!final_source_line or transcript_ends_with_newline or std.meta.activeTag(visible_lines[row.line_index]) == .folded_line); if (terminate_line) { try output.writer.writeAll("\r\n"); try presentation.feed("\r\n"); } }
try advanceResetReplayDocumentCursor( line_visual_rows, &start_line, &partial_skip_rows, rendered.painted_rows, ); remaining_visual_rows -= rendered_visual_rows; }
var padding_row: u16 = 0; while (padding_row < padding_rows) : (padding_row += 1) { try output.writer.writeAll("\r\n"); try presentation.feed("\r\n"); }
try presentation.writePresentationSteady(&output.writer); return output.toOwnedSlice();}
fn advanceResetReplayDocumentCursor( line_visual_rows: []const u16, start_line: *usize, partial_skip_rows: *u16, rows: u16,) !void { var remaining = rows; while (remaining > 0) { if (start_line.* >= line_visual_rows.len) return error.InvalidTranscriptTransition; const line_rows = line_visual_rows[start_line.*]; if (line_rows == 0 or partial_skip_rows.* >= line_rows) { return error.InvalidTranscriptTransition; } const available_rows = line_rows - partial_skip_rows.*; if (remaining < available_rows) { partial_skip_rows.* += remaining; return; } remaining -= available_rows; start_line.* += 1; partial_skip_rows.* = 0; }}
test "reset replay document preserves folded rows without filler" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "hidden raw source\ntail", 24); defer batch.deinit(alloc); batch.lines[0] = .{ .folded_line = .{ .text = "folded-prefix", .stream = .stdout } }; batch.line_visual_rows[0] = 1;
var host = TestTranscriptPainterHost.init(alloc, testLayout(24, 8, 4)); defer host.deinit(); const replay = try prepareResetReplayDocument( &host, alloc, batch.transcript_bytes, batch.lines, batch.line_visual_rows, false, 0, ); defer if (replay.len > 0) alloc.free(replay);
try std.testing.expect(std.mem.indexOf(u8, replay, "│ folded-prefix") != null); try std.testing.expect(std.mem.indexOf(u8, replay, "tail") != null); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, replay, "\r\n"));
var terminal = try vt_emulator.Grid.init(alloc, 24, 1); defer terminal.deinit(); var stats: vt_emulator.FeedStats = .{}; try terminal.feedWithStats(replay, &stats); try std.testing.expectEqual(@as(u16, 1), stats.scroll_rows);}
test "reset replay document fills an unused terminal tail without scrolling blanks" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "head\ntail", 10); defer batch.deinit(alloc);
var host = TestTranscriptPainterHost.init(alloc, testLayout(10, 8, 4)); defer host.deinit(); const replay = try prepareResetReplayDocument( &host, alloc, batch.transcript_bytes, batch.lines, batch.line_visual_rows, false, 2, ); defer if (replay.len > 0) alloc.free(replay);
var terminal = try vt_emulator.Grid.init(alloc, 10, 4); defer terminal.deinit(); var stats: vt_emulator.FeedStats = .{}; try terminal.feedWithStats(replay, &stats); try std.testing.expectEqual(@as(u16, 0), stats.scroll_rows); var row: std.ArrayList(u8) = .empty; defer row.deinit(alloc); try terminal.rowTextTrimmed(1, &row); try std.testing.expectEqualStrings("head", row.items); row.clearRetainingCapacity(); try terminal.rowTextTrimmed(2, &row); try std.testing.expectEqualStrings("tail", row.items);}
test "reset replay document crosses the u16 row chunk boundary" { const alloc = std.testing.allocator; const head = try alloc.alloc(u8, std.math.maxInt(u16)); defer alloc.free(head); @memset(head, 'x'); const transcript = try std.mem.concat(alloc, u8, &.{ head, "\ntail" }); defer alloc.free(transcript); var batch = try transcriptTestBatch(alloc, transcript, 1); defer batch.deinit(alloc); try std.testing.expectEqual( @as(u32, std.math.maxInt(u16)) + 4, totalVisualRows(batch.line_visual_rows), );
var host = TestTranscriptPainterHost.init(alloc, testLayout(1, 8, 4)); defer host.deinit(); const replay = try prepareResetReplayDocument( &host, alloc, batch.transcript_bytes, batch.lines, batch.line_visual_rows, false, 0, ); defer if (replay.len > 0) alloc.free(replay);
try std.testing.expect(std.mem.indexOf(u8, replay, "tail") != null);}
fn preparedProjectionBoundary( prepared: *const PreparedTranscriptSurfacePaint, cols: u16, visual_end: u32,) ?PreparedProjectionBoundary { const line_visual_rows = prepared.sourceVisualRows(); const visible_lines = prepared.sourceVisibleLines(); if (line_visual_rows.len != visible_lines.len) return null; if (visual_end == 0) { return .{ .flow_end = 0, .line_terminated = false }; } if (prepared.source_visual_row_offsets.len == line_visual_rows.len + 1) { const position = sourceEndPosition(prepared, visual_end) orelse return null; if (position.line >= visible_lines.len) return null; const line = switch (visible_lines[position.line]) { .transcript => |value| value, .folded_line => return null, }; const text = line.ref.resolve(.{ .bytes = prepared.bytes }); const start: usize = line.ref.ref.start; if (position.intra_line_rows < line_visual_rows[position.line]) { return .{ .flow_end = start + skipVisualRowsInLine( text, cols, position.intra_line_rows, ), .line_terminated = false, }; } var end = start + line.ref.ref.len; if (end < prepared.bytes.len and prepared.bytes[end] == '\n') end += 1; return .{ .flow_end = end, .line_terminated = end > start and prepared.bytes[end - 1] == '\n', }; }
var remaining = visual_end; var flow_exact = true; for (visible_lines, line_visual_rows) |visible, line_rows| { if (remaining > line_rows) { remaining -= line_rows; if (std.meta.activeTag(visible) == .folded_line) flow_exact = false; continue; }
return switch (visible) { .transcript => |line| blk: { const text = line.ref.resolve(.{ .bytes = prepared.bytes }); const start: usize = line.ref.ref.start; if (remaining < line_rows) { const local_end = skipVisualRowsInLine( text, cols, @intCast(remaining), ); break :blk .{ .flow_end = if (flow_exact) start + local_end else null, .line_terminated = false, }; }
var end = start + line.ref.ref.len; if (end < prepared.bytes.len and prepared.bytes[end] == '\n') { end += 1; } break :blk .{ .flow_end = if (flow_exact) end else null, .line_terminated = end > start and prepared.bytes[end - 1] == '\n', }; }, .folded_line => .{ .flow_end = null, .line_terminated = remaining == line_rows, }, }; } if (remaining != 0) return null; return .{ .flow_end = prepared.bytes.len, .line_terminated = prepared.transcript_ends_with_newline, };}
fn projectionRemainingVisualRows( prepared: *const PreparedTranscriptSurfacePaint, visual_offset: u32,) ?u32 { const total_rows = prepared.sourceTotalVisualRows(); if (visual_offset > total_rows) return null; return total_rows - visual_offset;}
pub fn preparedTranscriptProjectionExceedsArea( prepared: *const PreparedTranscriptSurfacePaint, area: render_engine.frame_layout.FrameRect, visual_offset: u32,) bool { if (area.isEmpty()) return false; const remaining_visual_rows = projectionRemainingVisualRows(prepared, visual_offset) orelse return false; return remaining_visual_rows > area.height();}
pub fn paintPreparedTranscriptIntoSurface( self: anytype, alloc: Allocator, surface: *frame_surface.FrameSurface, prepared: *const PreparedTranscriptSurfacePaint,) !paint_plan.TranscriptPaintResult { const render_context = .{ .layout = self.layout, .replaceable_last_line = prepared.replaceable_last_line, .replaceable_start = prepared.replaceable_start, .replaceable_row = prepared.replaceable_row, }; const result = try paintTranscriptIntoSurfaceWithLimit( &render_context, alloc, surface, prepared.batch(), prepared.visualRows(), prepared.total_lines, prepared.projection_ends_with_newline orelse prepared.transcript_ends_with_newline, prepared.projection_visual_rows, prepared.projection_resume_bytes, prepared.projection_start_flow_offset, ); if (prepared.projection_cursor) |cursor| { surface.cursor_target = .{ .row = cursor.cursor_row, .col = cursor.cursor_col, .visible = true, }; } return result;}
pub fn paintTranscriptIntoSurface( self: anytype, alloc: Allocator, surface: *frame_surface.FrameSurface, batch: VisibleTranscriptBatch, line_visual_rows: []const u16, total_lines: usize, transcript_ends_with_newline: bool,) !paint_plan.TranscriptPaintResult { return paintTranscriptIntoSurfaceWithLimit( self, alloc, surface, batch, line_visual_rows, total_lines, transcript_ends_with_newline, null, &.{}, null, );}
fn paintTranscriptIntoSurfaceWithLimit( self: anytype, alloc: Allocator, surface: *frame_surface.FrameSurface, batch: VisibleTranscriptBatch, line_visual_rows: []const u16, total_lines: usize, transcript_ends_with_newline: bool, max_visual_rows: ?u16, projection_resume_bytes: []const u8, projection_start_flow_offset: ?usize,) !paint_plan.TranscriptPaintResult { const viewport = surface.plan.viewport; if (total_lines == 0) { return .{ .painted_rows = 0, .first_row = 0, .last_row = 0, .replaceable_start_row = viewport.top_row, }; }
var rendered_rows = try renderTranscriptRows( self, alloc, batch, line_visual_rows, total_lines, viewport.start_line, viewport.partial_skip_rows, viewport.top_row, viewport.bottom_row, max_visual_rows, projection_resume_bytes, projection_start_flow_offset, ); defer rendered_rows.deinit(alloc);
for (rendered_rows.rows.items) |rendered| { const last_row = rendered.row + rendered.rows_painted - 1; if (!surface.plan.transcript_band.containsRow(rendered.row) or !surface.plan.transcript_band.containsRow(last_row)) { return error.WriteOutsideBand; } const write_result = try surface.writeAnsiBand(rendered.row, rendered.rows_painted, rendered.bytes, .transcript, .same_owner); if (write_result.rows_touched != rendered.rows_painted or write_result.first_row != rendered.row or write_result.last_row != last_row) { return error.AnsiBandOverflow; } }
const cursor = calculateViewportCursor( self.layout, viewport.bottom_row, total_lines, viewport.start_line, rendered_rows.replaceable_line_index, rendered_rows.replaceable_start_row, rendered_rows.last_visible_line, rendered_rows.last_visible_row, rendered_rows.last_line_is_folded, viewport.top_row, transcript_ends_with_newline, self.replaceable_last_line, self.replaceable_row, ); surface.cursor_target = .{ .row = cursor.cursor_row, .col = cursor.cursor_col, .visible = true };
return .{ .painted_rows = rendered_rows.painted_rows, .first_row = if (rendered_rows.rows.items.len == 0) 0 else rendered_rows.rows.items[0].row, .last_row = rendered_rows.last_visible_row, .replaceable_start_row = rendered_rows.replaceable_start_row, };}
const TestFooterGeometry = struct { top: u16 = 0,};
const TestFooterViewport = struct { externally_invalidated: bool = false, has_frame: bool = false, geometry: TestFooterGeometry = .{},
fn invalidateAfterExternalClear(self: *TestFooterViewport) void { self.externally_invalidated = true; }};
const TestPaintTrace = struct { transcript_log_frame: bool = false,};
const TestTranscriptPainterHost = struct { alloc: Allocator, layout: types.Layout, shadow: ?*vt_emulator.Grid = null, emitted: std.ArrayList(u8) = .empty, footer_viewport: TestFooterViewport = .{}, paint_trace: TestPaintTrace = .{}, reflow_clear_guard_rows: u16 = 0, viewport_clear_pending: bool = false, owned_top_row: u16 = 1, last_visible_transcript_last_row: u16 = 0, last_visible_transcript_last_row_blank: bool = false, cursor_row: u16 = 1, cursor_col: u16 = 1, extra_input_rows: u16 = 0, replaceable_last_line: bool = false, replaceable_start: usize = 0, replaceable_row: u16 = 1,
fn init(alloc: Allocator, layout: types.Layout) TestTranscriptPainterHost { return .{ .alloc = alloc, .layout = layout, .cursor_row = if (layout.content_bottom == 0) 1 else layout.content_bottom, }; }
fn deinit(self: *TestTranscriptPainterHost) void { self.emitted.deinit(self.alloc); }
fn emit(self: *TestTranscriptPainterHost, _: *Metrics, bytes: []const u8) !void { try self.emitted.appendSlice(self.alloc, bytes); if (self.shadow) |shadow| try shadow.feed(bytes); }
fn footerTopRowForExtra(self: *TestTranscriptPainterHost, _: u16) u16 { return self.layout.divider_top_row; }};
const TestTranscriptBatch = struct { transcript_bytes: []u8, hard_lines: ?HardLineStarts = null, lines: []VisibleTranscriptLine, line_visual_rows: []u16, ends_with_newline: bool = false,
fn deinit(self: *TestTranscriptBatch, alloc: Allocator) void { if (self.hard_lines) |hard_lines| hard_lines.deinit(alloc); alloc.free(self.transcript_bytes); alloc.free(self.lines); alloc.free(self.line_visual_rows); }
fn batch(self: TestTranscriptBatch) VisibleTranscriptBatch { return .{ .transcript = .{ .bytes = self.transcript_bytes }, .lines = self.lines, }; }};
fn testLayout(cols: u16, rows: u16, content_bottom: u16) types.Layout { return .{ .rows = rows, .cols = cols, .content_bottom = content_bottom, .divider_top_row = content_bottom + 1, .input_row = content_bottom + 2, .divider_bottom_row = content_bottom + 3, .hint_row = rows, };}
fn transcriptTestBatch(alloc: Allocator, bytes: []const u8, cols: u16) !TestTranscriptBatch { const owned = try alloc.dupe(u8, bytes); errdefer alloc.free(owned); const hard_lines = try buildHardLineStarts(alloc, owned); errdefer hard_lines.deinit(alloc); const line_count = hard_lines.len(); const lines = try alloc.alloc(VisibleTranscriptLine, line_count); errdefer alloc.free(lines); const line_visual_rows = try alloc.alloc(u16, line_count); errdefer alloc.free(line_visual_rows);
var index: usize = 0; while (index < line_count) : (index += 1) { const ref = hardLineRefAt(hard_lines, owned.len, index); lines[index] = visibleFromTranscript(ref); line_visual_rows[index] = visualRowsForLine(ref.resolve(owned), cols); }
return .{ .transcript_bytes = owned, .hard_lines = hard_lines, .lines = lines, .line_visual_rows = line_visual_rows, .ends_with_newline = hard_lines.ends_with_newline, };}
fn foldedTestBatch( alloc: Allocator, stdout_text: []const u8, stderr_text: []const u8, cols: u16,) !TestTranscriptBatch { const lines = try alloc.alloc(VisibleTranscriptLine, 2); errdefer alloc.free(lines); const line_visual_rows = try alloc.alloc(u16, 2); errdefer alloc.free(line_visual_rows); lines[0] = .{ .folded_line = .{ .text = stdout_text, .stream = .stdout } }; lines[1] = .{ .folded_line = .{ .text = stderr_text, .stream = .stderr } }; const effective_cols: u16 = if (cols > 2) cols - 2 else cols; line_visual_rows[0] = visualRowsForLine(stripTrailingNewline(stdout_text), effective_cols); line_visual_rows[1] = visualRowsForLine(stripTrailingNewline(stderr_text), effective_cols); return .{ .transcript_bytes = try alloc.dupe(u8, ""), .lines = lines, .line_visual_rows = line_visual_rows, };}
fn testFooterRows(layout: types.Layout) render_engine.footer_layout.FooterRows { return .{ .top = layout.divider_top_row, .top_divider = layout.divider_top_row, .banner = layout.divider_top_row, .banner_active = false, .input_base = layout.input_row, .picker_divider = layout.divider_bottom_row, .picker_start = layout.hint_row, .bottom_divider = layout.divider_bottom_row, .hint = layout.hint_row, .total_rows = layout.rows - layout.divider_top_row + 1, };}
fn testPaintPlan(layout: types.Layout, selection: ViewportSelection) paint_plan.PaintPlan { const preserved_band = if (selection.top_row > 1) paint_plan.FrameBand{ .top = 1, .bottom = selection.top_row - 1, .owner = .preserved_shell } else paint_plan.FrameBand.empty(.preserved_shell); return .{ .layout = layout, .viewport = selection, .footer = testFooterRows(layout), .activity = .none, .preserved_band = preserved_band, .transcript_band = .{ .top = selection.top_row, .bottom = selection.bottom_row, .owner = .transcript }, .activity_band = paint_plan.FrameBand.empty(.activity), .footer_band = .{ .top = layout.divider_top_row, .bottom = layout.rows, .owner = .footer }, .invalidation = paint_plan.FrameInvalidationSet.empty(), .footer_clean_allowed = true, .synchronized_update = false, .cursor_target = null, .bottom_reserved_rows = 0, .preserve_scrollback = true, };}
fn expectTranscriptSurfacePaint( alloc: Allocator, layout: types.Layout, selection: ViewportSelection, batch: TestTranscriptBatch,) !vt_emulator.Grid { var surface_shadow = try vt_emulator.Grid.init(alloc, layout.cols, layout.rows); defer surface_shadow.deinit(); var surface = try frame_surface.FrameSurface.initFromShadow(alloc, testPaintPlan(layout, selection), surface_shadow); defer surface.deinit(); var surface_host = TestTranscriptPainterHost.init(alloc, layout); defer surface_host.deinit(); const result = try paintTranscriptIntoSurface( &surface_host, alloc, &surface, batch.batch(), batch.line_visual_rows, batch.lines.len, batch.ends_with_newline, ); var target = try surface.copyToTargetGrid(alloc); errdefer target.deinit();
try std.testing.expect(result.first_row >= selection.top_row); try std.testing.expect(result.last_row <= selection.bottom_row); try std.testing.expect(result.replaceable_start_row >= selection.top_row); try std.testing.expectEqual(@as(u16, @intCast(result.last_row - selection.top_row + 1)), result.painted_rows); const cursor = surface.cursor_target.?; try std.testing.expect(cursor.row >= selection.top_row); try std.testing.expect(cursor.row <= selection.bottom_row); try std.testing.expect(cursor.col >= 1); try surface.validate();
return target;}
fn expectGridContains(grid: *vt_emulator.Grid, alloc: Allocator, needle: []const u8) !void { var row_text: std.ArrayList(u8) = .empty; defer row_text.deinit(alloc); var row: u16 = 1; while (row <= grid.rows) : (row += 1) { row_text.clearRetainingCapacity(); try grid.rowText(row, &row_text); if (std.mem.find(u8, row_text.items, needle) != null) return; } return error.ExpectedTextNotFound;}
test "resize history offset preserves the measured intra-line endpoint" { const rows = [_]u16{ 1, 1, 2, 1 }; var selection = ViewportSelection{ .top_row = 1, .bottom_row = 8, .start_line = 0, .partial_skip_rows = 0, .line_count = rows.len, };
try std.testing.expect(applyResizeHistoryOffset(&selection, &rows, 3)); try std.testing.expectEqual(@as(usize, 2), selection.start_line); try std.testing.expectEqual(@as(u16, 1), selection.partial_skip_rows);}
test "measured source rows release partial groups at a folded boundary" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "abcdefghij\nhidden", 5); defer batch.deinit(alloc); batch.lines[1] = .{ .folded_line = .{ .text = "folded", .stream = .stdout } };
const groups = try collectMeasuredSourceRows( alloc, batch.batch(), 5, 0, batch.transcript_bytes.len, ); try std.testing.expect(groups == null);}
test "measured history endpoint is unavailable when selection starts on folded output" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "summary\ntail", 8); defer batch.deinit(alloc); batch.lines[0] = .{ .folded_line = .{ .text = "folded", .stream = .stdout } };
const endpoint = try advanceMeasuredHistoryEndpoint( alloc, batch.batch(), .{ .top_row = 1, .bottom_row = 2, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, 8, 4, 1, ); try std.testing.expect(endpoint == null);}
test "measured history endpoint advances through exact wrapped source rows" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch( alloc, "abcdefghij\nklmnopqrst\nuvwxyz", 10, ); defer batch.deinit(alloc);
const endpoint = (try advanceMeasuredHistoryEndpoint( alloc, batch.batch(), .{ .top_row = 1, .bottom_row = 3, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, 10, 5, 1, )) orelse return error.TestExpectedMeasuredEndpoint; try std.testing.expectEqual(@as(usize, 5), endpoint.flow_offset); const target_rows = [_]u16{ 2, 2, 2 }; try std.testing.expectEqual( @as(?u32, 1), visualOffsetForExactFlowOffset( batch.batch(), &target_rows, 5, endpoint.flow_offset, ), );}
test "rewinding a measured endpoint returns to the advance origin" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch( alloc, "abcdefghij\nklmnopqrst\nuvwxyz", 10, ); defer batch.deinit(alloc);
const endpoint = (try advanceMeasuredHistoryEndpoint( alloc, batch.batch(), .{ .top_row = 1, .bottom_row = 3, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, 10, 5, 3, )) orelse return error.TestExpectedMeasuredEndpoint; try std.testing.expectEqual(@as(usize, 16), endpoint.flow_offset);
const rewound = try rewindMeasuredHistoryEndpoint( alloc, batch.batch(), endpoint, 5, 3, ); try std.testing.expectEqual( @as(?usize, endpoint.span_start_flow_offset), rewound, );
const rewound_one = try rewindMeasuredHistoryEndpoint( alloc, batch.batch(), endpoint, 5, 1, ); try std.testing.expectEqual(@as(?usize, 11), rewound_one);}
test "empty hard line start maps to its exact visual row" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "head\n\ntail", 10); defer batch.deinit(alloc);
const projection = visualProjectionForFlowOffset( batch.batch(), batch.line_visual_rows, 10, 5, ) orelse return error.TestExpectedFlowProjection; try std.testing.expectEqual(@as(u32, 1), projection.visual_offset); try std.testing.expect(projection.exact); try std.testing.expectEqual( @as(?u32, 1), visualOffsetForExactFlowOffset( batch.batch(), batch.line_visual_rows, 10, 5, ), );}
test "inexact measured endpoint resumes from its containing target row" { const alloc = std.testing.allocator; const record = "a" ** 249; const flow = record ++ "\n" ++ record ++ "\n" ++ record ++ "\n" ++ record ++ "\n" ++ record ++ "\n" ++ record; var batch = try transcriptTestBatch(alloc, flow, 70); defer batch.deinit(alloc);
const endpoint = (try advanceMeasuredHistoryEndpoint( alloc, batch.batch(), .{ .top_row = 1, .bottom_row = 30, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, 120, 70, 29, )) orelse return error.TestExpectedMeasuredEndpoint; try std.testing.expectEqual(@as(usize, 1490), endpoint.flow_offset);
const projection = visualProjectionForFlowOffset( batch.batch(), batch.line_visual_rows, 70, endpoint.flow_offset, ) orelse return error.TestExpectedFlowProjection; try std.testing.expectEqual(@as(u32, 23), projection.visual_offset); try std.testing.expect(!projection.exact);
const layout = testLayout(70, 24, 20); var host = TestTranscriptPainterHost.init(alloc, layout); defer host.deinit(); var rendered = try renderTranscriptRows( &host, alloc, batch.batch(), batch.line_visual_rows, batch.lines.len, 5, 3, 1, 20, null, &.{}, endpoint.flow_offset, ); defer rendered.deinit(alloc); try std.testing.expectEqual(@as(usize, 9), rendered.rows.items[0].bytes.len); try std.testing.expectEqualStrings("a" ** 9, rendered.rows.items[0].bytes); try std.testing.expectEqual(@as(u16, 1), rendered.rows.items[0].rows_painted); try std.testing.expectEqualStrings("a" ** 9, rendered.last_visible_line); const cursor = calculateViewportCursor( layout, 20, batch.lines.len, 5, batch.lines.len, 1, rendered.last_visible_line, rendered.last_visible_row, false, 1, false, false, 1, ); try std.testing.expectEqual(@as(u16, 1), cursor.cursor_row); try std.testing.expectEqual(@as(u16, 10), cursor.cursor_col);}
test "committed measured endpoint advances by source hard-row groups" { const flow = ("a" ** 21) ++ "\n" ++ ("b" ** 21) ++ "\n"; const advanced = advanceMeasuredHistoryOriginForCommittedRows( flow, .{ .source_cols = 10, .source_visual_offset = 0, .endpoint = .{ .span_start_flow_offset = 0, .flow_offset = 0, .hard_row_cols = 10, }, }, 6, 5, ) orelse return error.TestExpectedMeasuredHistoryOrigin; try std.testing.expectEqual(@as(usize, 22), advanced.endpoint.?.flow_offset);}
test "exact measured endpoint mapping is inexact when folded output precedes it" { const alloc = std.testing.allocator; var source = try transcriptTestBatch(alloc, "head\ntail", 8); defer source.deinit(alloc); const endpoint = (try advanceMeasuredHistoryEndpoint( alloc, source.batch(), .{ .top_row = 1, .bottom_row = 2, .start_line = 1, .partial_skip_rows = 0, .line_count = source.lines.len, }, 8, 4, 1, )) orelse return error.TestExpectedMeasuredEndpoint;
source.lines[0] = .{ .folded_line = .{ .text = "folded", .stream = .stdout } }; try std.testing.expect( visualOffsetForExactFlowOffset( source.batch(), source.line_visual_rows, 4, endpoint.flow_offset, ) == null, );}
test "resize history offset drops a pinned welcome split" { const rows = [_]u16{ 1, 1, 1, 1, 1 }; var selection = ViewportSelection{ .top_row = 1, .bottom_row = 4, .start_line = 0, .partial_skip_rows = 0, .line_count = 4, .split_active = true, .split_prefix_lines = 2, .split_suffix_start_line = 3, };
_ = applyResizeHistoryOffset(&selection, &rows, 2); try std.testing.expect(!selection.split_active); try std.testing.expectEqual(@as(usize, 3), selection.start_line);}
test "resize history offset applies measured rows to the committed prefix" { try std.testing.expectEqual(@as(u32, 8), offsetBySignedRows(8, 0)); try std.testing.expectEqual(@as(u32, 12), offsetBySignedRows(4, 8)); try std.testing.expectEqual(@as(u32, 0), offsetBySignedRows(4, -10));}
test "resize history offset replays the welcome tail when history returns" { const rows = [_]u16{ 1, 1, 1, 1, 2, 1, 1, 2 }; const boundary = transcript_blocks.LeadingWelcomeBoundary{ .full_cut_line = 7, .tail_replay_line = 5, }; try std.testing.expectEqual( @as(u32, 8), preserveWelcomeReplayBoundary(4, 4, boundary, &rows), ); try std.testing.expectEqual( @as(u32, 6), preserveWelcomeReplayBoundary(4, -2, boundary, &rows), );}
test "transcript surface painter renders welcome plus user rows" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "fx welcome\n\n╭ user prompt\n╰ done", 24); defer batch.deinit(alloc); const layout = testLayout(24, 10, 6); var target = try expectTranscriptSurfacePaint(alloc, layout, .{ .top_row = 1, .bottom_row = 6, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, batch); defer target.deinit(); try expectGridContains(&target, alloc, "fx welcome"); try expectGridContains(&target, alloc, "user prompt");}
test "transcript surface painter renders assistant partial tail" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "abcdefghijklmnopqrst", 5); defer batch.deinit(alloc); const layout = testLayout(5, 8, 4); var target = try expectTranscriptSurfacePaint(alloc, layout, .{ .top_row = 2, .bottom_row = 3, .start_line = 0, .partial_skip_rows = 2, .line_count = batch.lines.len, }, batch); defer target.deinit(); try expectGridContains(&target, alloc, "klmno"); try expectGridContains(&target, alloc, "pqrst");}
test "transcript surface painter renders soft wrap across three rows" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "abcdefghijkl", 5); defer batch.deinit(alloc); const layout = testLayout(5, 8, 4); var target = try expectTranscriptSurfacePaint(alloc, layout, .{ .top_row = 1, .bottom_row = 3, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, batch); defer target.deinit(); try expectGridContains(&target, alloc, "abcde"); try expectGridContains(&target, alloc, "fghij"); try expectGridContains(&target, alloc, "kl");}
test "transcript surface painter materializes an ANSI-only logical row" { const alloc = std.testing.allocator; const styled_notice = "\x1b[2m" ++ ("x" ** 391) ++ "\x1b[0m"; try std.testing.expectEqual(@as(usize, 399), styled_notice.len);
var batch = try transcriptTestBatch(alloc, styled_notice ++ "\n\x1b[0m", 123); defer batch.deinit(alloc); try std.testing.expectEqualSlices(u16, &.{ 4, 1 }, batch.line_visual_rows);
const layout = testLayout(123, 34, 30); var target = try expectTranscriptSurfacePaint(alloc, layout, .{ .top_row = 1, .bottom_row = 5, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, batch); defer target.deinit(); try expectGridContains(&target, alloc, "xxxxxxxx");}
test "ANSI-only transcript bytes retain controls and materialize their planned row" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "\x1b[0m", 123); defer batch.deinit(alloc); const layout = testLayout(123, 34, 30); var host = TestTranscriptPainterHost.init(alloc, layout); defer host.deinit();
var rendered = try renderTranscriptRows( &host, alloc, batch.batch(), batch.line_visual_rows, batch.lines.len, 0, 0, 1, 1, null, &.{}, null, ); defer rendered.deinit(alloc); try std.testing.expectEqual(@as(u16, 1), rendered.rows.items[0].rows_painted); try std.testing.expectEqualStrings("\x1b[0m\x1b[2K", rendered.rows.items[0].bytes);}
test "transcript surface painter renders hard newline row boundaries" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "alpha\nbeta\n\ngamma", 20); defer batch.deinit(alloc); const layout = testLayout(20, 10, 6); var target = try expectTranscriptSurfacePaint(alloc, layout, .{ .top_row = 1, .bottom_row = 6, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, batch); defer target.deinit(); try expectGridContains(&target, alloc, "alpha"); try expectGridContains(&target, alloc, "beta"); try expectGridContains(&target, alloc, "gamma");}
test "transcript surface painter renders folded stdout and stderr" { const alloc = std.testing.allocator; var batch = try foldedTestBatch(alloc, "stdout wraps here", "stderr wraps here", 10); defer batch.deinit(alloc); const layout = testLayout(10, 10, 6); var target = try expectTranscriptSurfacePaint(alloc, layout, .{ .top_row = 1, .bottom_row = 6, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, batch); defer target.deinit(); try expectGridContains(&target, alloc, "stdout"); try expectGridContains(&target, alloc, "stderr");}
test "transcript surface painter renders block gap rows" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "raw block\n\nassistant block\n\n╭ user block", 28); defer batch.deinit(alloc); const layout = testLayout(28, 10, 7); var target = try expectTranscriptSurfacePaint(alloc, layout, .{ .top_row = 1, .bottom_row = 7, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, batch); defer target.deinit(); try expectGridContains(&target, alloc, "raw block"); try expectGridContains(&target, alloc, "assistant block"); try expectGridContains(&target, alloc, "user block");}
test "transcript surface painter preserves OSC 8 links through target-grid diff" { const alloc = std.testing.allocator; const linked = "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\"; var batch = try transcriptTestBatch(alloc, linked, 20); defer batch.deinit(alloc); const layout = testLayout(20, 8, 4); var target = try expectTranscriptSurfacePaint(alloc, layout, .{ .top_row = 1, .bottom_row = 4, .start_line = 0, .partial_skip_rows = 0, .line_count = batch.lines.len, }, batch); defer target.deinit();
const target_cell = target.cellAt(1, 1).?; try std.testing.expect(target_cell.style.hyperlink_id != 0); try std.testing.expectEqualStrings("https://example.com", target.hyperlinkUrl(target_cell.style.hyperlink_id).?);
var diff_prev = try vt_emulator.Grid.init(alloc, layout.cols, layout.rows); defer diff_prev.deinit(); var diff_buf: std.ArrayList(u8) = .empty; defer diff_buf.deinit(alloc); var writer = std.Io.Writer.Allocating.fromArrayList(alloc, &diff_buf); try vt_emulator.Grid.diffBand(diff_prev, target, 1, 1, &writer.writer); diff_buf = writer.toArrayList(); try diff_prev.feed(diff_buf.items); const diff_cell = diff_prev.cellAt(1, 1).?; try std.testing.expectEqualStrings("https://example.com", diff_prev.hyperlinkUrl(diff_cell.style.hyperlink_id).?);}
fn expectAppendBoundaryAllocation(alloc: Allocator) !void { const raw = "\x1b[31m\x1b]8;;https://example.com\x1b\\12345678X\nY\n"; const start = raw.len - "X\nY\n".len; var prepared = prepareTranscriptDocumentAppend(alloc, raw, 8, start, raw.len, true) catch |err| switch (err) { error.WriteFailed => return error.OutOfMemory, else => return err, }; defer prepared.deinit(alloc); try std.testing.expect(prepared.start_pending_wrap); try std.testing.expect(std.mem.find(u8, prepared.bytes, "X\r\nY\r\n") != null);}
test "append pending wrap preparation allocation failures release owned boundaries" { try std.testing.checkAllAllocationFailures(std.testing.allocator, expectAppendBoundaryAllocation, .{});}
test "resume document append matches full prefix replay" { const alloc = std.testing.allocator; const prefix = "\x1b[31mABCD"; const middle = "\x1b[32mEFGH\nI"; const flow = prefix ++ middle ++ "\x1b[0mJ";
var start = try vt_emulator.Grid.init(alloc, 4, 1); defer start.deinit(); try start.feed(prefix); var start_resume: std.Io.Writer.Allocating = .init(alloc); defer start_resume.deinit(); try start.writePresentationResume(&start_resume.writer);
var append = try prepareResumeDocumentAppend( alloc, flow, 4, prefix.len, prefix.len + middle.len, true, .{ .resume_bytes = start_resume.written(), .cursor_col = start.cursor_col, .pending_wrap = start.pending_wrap, }, true, ); defer append.deinit(alloc); const replay = try prepareTranscriptDocumentAppendBytes( alloc, flow, 4, prefix.len, prefix.len + middle.len, true, ); defer if (replay.len > 0) alloc.free(replay);
try std.testing.expectEqualStrings(replay, append.bytes); var endpoint = try vt_emulator.Grid.init(alloc, 4, 1); defer endpoint.deinit(); try endpoint.feed(flow[0 .. prefix.len + middle.len]); var endpoint_resume: std.Io.Writer.Allocating = .init(alloc); defer endpoint_resume.deinit(); try endpoint.writePresentationResume(&endpoint_resume.writer); try std.testing.expectEqualStrings(endpoint_resume.written(), append.endpoint_resume_bytes); try std.testing.expectEqual(endpoint.cursor_col, append.endpoint_cursor_col); try std.testing.expectEqual(endpoint.pending_wrap, append.endpoint_pending_wrap);}
test "transcript surface painter handles zero transcript lines" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "", 20); defer batch.deinit(alloc); const layout = testLayout(20, 8, 4); const selection: ViewportSelection = .{ .top_row = 1, .bottom_row = 4, .start_line = 0, .partial_skip_rows = 0, .line_count = 0, }; var shadow = try vt_emulator.Grid.init(alloc, layout.cols, layout.rows); defer shadow.deinit(); var surface = try frame_surface.FrameSurface.initFromShadow(alloc, testPaintPlan(layout, selection), shadow); defer surface.deinit(); var host = TestTranscriptPainterHost.init(alloc, layout); defer host.deinit();
const result = try paintTranscriptIntoSurface(&host, alloc, &surface, batch.batch(), batch.line_visual_rows, 0, false); try std.testing.expectEqual(@as(u16, 0), result.painted_rows); try std.testing.expectEqual(@as(u16, 0), result.first_row); try std.testing.expectEqual(@as(u16, 0), result.last_row); try std.testing.expect(surface.cursor_target == null);}
test "transcript selection bottom follows sparse rendered rows" { const alloc = std.testing.allocator; var batch = try transcriptTestBatch(alloc, "alpha\nbeta\n", 20); defer batch.deinit(alloc); const layout = testLayout(20, 12, 8); var host = TestTranscriptPainterHost.init(alloc, layout); defer host.deinit();
var selection = buildViewportSelection(.{ .top_row = 1, .bottom_row = 8, .line_visual_rows = batch.line_visual_rows, .visible_rows = 8, .replaceable_line_index = batch.lines.len, }).selection; try std.testing.expectEqual(@as(u16, 8), selection.bottom_row);
var rendered_rows = try renderTranscriptRows( &host, alloc, batch.batch(), batch.line_visual_rows, batch.lines.len, selection.start_line, selection.partial_skip_rows, selection.top_row, selection.bottom_row, null, &.{}, null, ); defer rendered_rows.deinit(alloc);
applyRenderedRowsToSelection(&selection, &rendered_rows); try std.testing.expectEqual(rendered_rows.last_visible_row, selection.last_visible_row); try std.testing.expectEqual(rendered_rows.last_visible_row, selection.bottom_row); try std.testing.expectEqual(@as(u16, 2), selection.bottom_row);}
test "transcript document control cursor matches independent boundary preparation" { const alloc = std.testing.allocator; const bytes = "\x1b[31mred-red\nstill-red\x1b[0m\n" ++ "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\\nplain"; const ranges = [_][2]usize{ .{ 5, 12 }, .{ 13, 26 }, .{ 27, 64 }, .{ 65, 70 }, };
var cursor = try TranscriptDocumentControlCursor.init(alloc, bytes, 20); defer cursor.deinit(); for (ranges) |range| { const expected = try prepareTranscriptDocumentAppendBytes( alloc, bytes, 20, range[0], range[1], true, ); defer if (expected.len > 0) alloc.free(expected); const actual = try cursor.prepareAppendBytes( alloc, range[0], range[1], true, ); defer if (actual.len > 0) alloc.free(actual); try std.testing.expectEqualStrings(expected, actual); }}
test "transcript paint row traces stay metadata only" { const source = @embedFile("painter.zig"); const forbidden_preview = "pre" ++ "view="; try std.testing.expect(std.mem.find(u8, source, "transcript_line row=") != null); try std.testing.expect(std.mem.find(u8, source, forbidden_preview) == null);}