Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666966796689669967096719672967396749675967696779678967996809681968296839684968596869687968896899690969196929693969496959696969796989699970097019702970397049705970697079708970997109711971297139714971597169717971897199720972197229723972497259726972797289729973097319732973397349735973697379738973997409741974297439744974597469747974897499750975197529753975497559756975797589759976097619762976397649765976697679768976997709771977297739774977597769777977897799780978197829783978497859786978797889789979097919792979397949795979697979798979998009801980298039804980598069807980898099810981198129813981498159816981798189819982098219822982398249825982698279828982998309831983298339834983598369837983898399840984198429843984498459846984798489849985098519852985398549855985698579858985998609861986298639864986598669867986898699870987198729873987498759876987798789879988098819882988398849885988698879888988998909891989298939894989598969897989898999900990199029903990499059906990799089909991099119912991399149915991699179918991999209921992299239924992599269927992899299930993199329933993499359936993799389939994099419942994399449945994699479948994999509951995299539954995599569957995899599960996199629963996499659966996799689969997099719972997399749975997699779978997999809981998299839984998599869987998899899990999199929993999499959996999799989999100001000110002100031000410005100061000710008100091001010011100121001310014100151001610017100181001910020100211002210023100241002510026100271002810029100301003110032100331003410035100361003710038100391004010041100421004310044100451004610047100481004910050100511005210053100541005510056100571005810059100601006110062100631006410065100661006710068100691007010071100721007310074100751007610077100781007910080100811008210083100841008510086100871008810089100901009110092100931009410095100961009710098100991010010101101021010310104101051010610107101081010910110101111011210113101141011510116101171011810119101201012110122101231012410125101261012710128101291013010131101321013310134101351013610137101381013910140101411014210143101441014510146101471014810149101501015110152101531015410155101561015710158101591016010161101621016310164101651016610167101681016910170101711017210173101741017510176101771017810179101801018110182101831018410185101861018710188101891019010191101921019310194101951019610197101981019910200102011020210203102041020510206102071020810209102101021110212102131021410215102161021710218102191022010221102221022310224102251022610227102281022910230102311023210233102341023510236102371023810239102401024110242102431024410245102461024710248102491025010251102521025310254102551025610257102581025910260102611026210263102641026510266102671026810269102701027110272102731027410275102761027710278102791028010281102821028310284102851028610287102881028910290102911029210293102941029510296102971029810299103001030110302103031030410305103061030710308103091031010311103121031310314103151031610317103181031910320103211032210323103241032510326103271032810329103301033110332103331033410335103361033710338103391034010341103421034310344103451034610347103481034910350103511035210353103541035510356103571035810359103601036110362103631036410365103661036710368103691037010371103721037310374103751037610377103781037910380103811038210383103841038510386103871038810389103901039110392103931039410395103961039710398103991040010401104021040310404104051040610407104081040910410104111041210413104141041510416104171041810419104201042110422104231042410425104261042710428104291043010431104321043310434104351043610437104381043910440104411044210443104441044510446104471044810449104501045110452104531045410455104561045710458104591046010461104621046310464104651046610467104681046910470104711047210473104741047510476104771047810479104801048110482104831048410485104861048710488104891049010491104921049310494104951049610497104981049910500105011050210503105041050510506105071050810509105101051110512105131051410515105161051710518105191052010521105221052310524105251052610527105281052910530105311053210533105341053510536105371053810539105401054110542105431054410545105461054710548105491055010551105521055310554105551055610557105581055910560105611056210563105641056510566105671056810569105701057110572105731057410575105761057710578105791058010581105821058310584105851058610587105881058910590105911059210593105941059510596105971059810599106001060110602106031060410605106061060710608106091061010611106121061310614106151061610617106181061910620106211062210623106241062510626106271062810629106301063110632106331063410635106361063710638106391064010641106421064310644106451064610647106481064910650106511065210653106541065510656106571065810659106601066110662106631066410665106661066710668106691067010671106721067310674106751067610677106781067910680106811068210683106841068510686106871068810689106901069110692106931069410695106961069710698106991070010701107021070310704107051070610707107081070910710107111071210713107141071510716107171071810719107201072110722107231072410725107261072710728107291073010731107321073310734107351073610737107381073910740107411074210743107441074510746107471074810749107501075110752107531075410755107561075710758107591076010761107621076310764107651076610767107681076910770107711077210773107741077510776107771077810779107801078110782107831078410785107861078710788107891079010791107921079310794107951079610797107981079910800108011080210803108041080510806108071080810809108101081110812108131081410815108161081710818108191082010821108221082310824108251082610827108281082910830108311083210833108341083510836108371083810839108401084110842108431084410845108461084710848108491085010851108521085310854108551085610857108581085910860108611086210863108641086510866108671086810869108701087110872108731087410875108761087710878108791088010881108821088310884108851088610887108881088910890108911089210893108941089510896108971089810899109001090110902109031090410905109061090710908109091091010911109121091310914109151091610917109181091910920109211092210923109241092510926109271092810929109301093110932109331093410935109361093710938109391094010941109421094310944109451094610947109481094910950109511095210953109541095510956109571095810959109601096110962109631096410965109661096710968109691097010971109721097310974109751097610977109781097910980109811098210983109841098510986109871098810989109901099110992109931099410995109961099710998109991100011001110021100311004110051100611007110081100911010110111101211013110141101511016110171101811019110201102111022110231102411025110261102711028110291103011031110321103311034110351103611037110381103911040110411104211043110441104511046110471104811049110501105111052110531105411055110561105711058110591106011061110621106311064110651106611067110681106911070110711107211073110741107511076110771107811079110801108111082110831108411085110861108711088110891109011091110921109311094110951109611097110981109911100111011110211103111041110511106111071110811109111101111111112111131111411115111161111711118111191112011121111221112311124111251112611127111281112911130111311113211133111341113511136111371113811139111401114111142111431114411145111461114711148111491115011151111521115311154111551115611157111581115911160111611116211163111641116511166111671116811169111701117111172111731117411175111761117711178111791118011181111821118311184111851118611187111881118911190111911119211193111941119511196111971119811199112001120111202112031120411205112061120711208112091121011211112121121311214112151121611217112181121911220112211122211223112241122511226112271122811229112301123111232112331123411235112361123711238112391124011241112421124311244112451124611247112481124911250112511125211253112541125511256112571125811259112601126111262112631126411265112661126711268112691127011271112721127311274112751127611277112781127911280112811128211283112841128511286112871128811289112901129111292112931129411295112961129711298112991130011301113021130311304113051130611307113081130911310113111131211313113141131511316113171131811319113201132111322113231132411325113261132711328113291133011331113321133311334113351133611337113381133911340113411134211343113441134511346113471134811349113501135111352113531135411355113561135711358113591136011361113621136311364113651136611367113681136911370113711137211373113741137511376113771137811379113801138111382113831138411385113861138711388113891139011391113921139311394113951139611397113981139911400114011140211403114041140511406114071140811409114101141111412114131141411415114161141711418114191142011421114221142311424114251142611427114281142911430114311143211433114341143511436114371143811439114401144111442114431144411445114461144711448114491145011451114521145311454114551145611457114581145911460114611146211463114641146511466114671146811469114701147111472114731147411475114761147711478114791148011481114821148311484114851148611487114881148911490114911149211493114941149511496114971149811499115001150111502115031150411505115061150711508115091151011511115121151311514115151151611517115181151911520115211152211523115241152511526115271152811529115301153111532115331153411535115361153711538115391154011541115421154311544115451154611547115481154911550115511155211553115541155511556115571155811559115601156111562115631156411565115661156711568115691157011571115721157311574115751157611577115781157911580115811158211583115841158511586115871158811589115901159111592115931159411595115961159711598115991160011601116021160311604116051160611607116081160911610116111161211613116141161511616116171161811619116201162111622116231162411625116261162711628116291163011631116321163311634116351163611637116381163911640116411164211643116441164511646116471164811649116501165111652116531165411655116561165711658116591166011661116621166311664116651166611667116681166911670116711167211673116741167511676116771167811679116801168111682116831168411685116861168711688116891169011691116921169311694116951169611697116981169911700117011170211703117041170511706117071170811709117101171111712117131171411715117161171711718117191172011721117221172311724117251172611727117281172911730117311173211733117341173511736117371173811739117401174111742117431174411745117461174711748117491175011751117521175311754117551175611757117581175911760117611176211763117641176511766117671176811769117701177111772117731177411775117761177711778117791178011781117821178311784117851178611787117881178911790117911179211793117941179511796117971179811799118001180111802118031180411805118061180711808118091181011811118121181311814118151181611817118181181911820118211182211823118241182511826118271182811829118301183111832118331183411835118361183711838118391184011841118421184311844118451184611847118481184911850118511185211853118541185511856118571185811859118601186111862118631186411865118661186711868118691187011871118721187311874118751187611877118781187911880118811188211883118841188511886118871188811889118901189111892118931189411895118961189711898118991190011901119021190311904119051190611907119081190911910119111191211913119141191511916119171191811919119201192111922119231192411925119261192711928119291193011931119321193311934119351193611937119381193911940119411194211943119441194511946119471194811949119501195111952119531195411955119561195711958119591196011961119621196311964119651196611967119681196911970119711197211973119741197511976119771197811979119801198111982119831198411985119861198711988119891199011991119921199311994119951199611997119981199912000120011200212003120041200512006120071200812009120101201112012120131201412015120161201712018120191202012021120221202312024120251202612027120281202912030120311203212033120341203512036120371203812039120401204112042120431204412045120461204712048120491205012051120521205312054120551205612057120581205912060120611206212063120641206512066120671206812069120701207112072120731207412075120761207712078120791208012081120821208312084120851208612087120881208912090120911209212093120941209512096120971209812099121001210112102121031210412105121061210712108121091211012111121121211312114121151211612117121181211912120121211212212123121241212512126121271212812129121301213112132121331213412135121361213712138121391214012141121421214312144121451214612147121481214912150121511215212153121541215512156121571215812159121601216112162121631216412165121661216712168121691217012171121721217312174121751217612177121781217912180121811218212183121841218512186121871218812189121901219112192121931219412195121961219712198121991220012201122021220312204122051220612207122081220912210122111221212213122141221512216122171221812219122201222112222122231222412225122261222712228122291223012231122321223312234122351223612237122381223912240122411224212243122441224512246122471224812249122501225112252122531225412255122561225712258122591226012261122621226312264122651226612267122681226912270122711227212273122741227512276122771227812279122801228112282122831228412285122861228712288122891229012291122921229312294122951229612297122981229912300123011230212303123041230512306123071230812309123101231112312123131231412315123161231712318123191232012321123221232312324123251232612327123281232912330123311233212333123341233512336123371233812339123401234112342123431234412345123461234712348123491235012351123521235312354123551235612357123581235912360123611236212363123641236512366123671236812369123701237112372123731237412375123761237712378123791238012381123821238312384123851238612387123881238912390123911239212393123941239512396123971239812399124001240112402124031240412405124061240712408124091241012411124121241312414124151241612417124181241912420124211242212423124241242512426124271242812429124301243112432124331243412435124361243712438124391244012441124421244312444124451244612447124481244912450124511245212453124541245512456124571245812459124601246112462124631246412465124661246712468124691247012471124721247312474124751247612477124781247912480124811248212483124841248512486124871248812489124901249112492124931249412495124961249712498124991250012501125021250312504125051250612507125081250912510125111251212513125141251512516125171251812519125201252112522125231252412525125261252712528125291253012531125321253312534125351253612537125381253912540125411254212543125441254512546125471254812549125501255112552125531255412555125561255712558125591256012561125621256312564125651256612567125681256912570125711257212573125741257512576125771257812579125801258112582125831258412585125861258712588125891259012591125921259312594125951259612597125981259912600126011260212603126041260512606126071260812609126101261112612126131261412615126161261712618126191262012621126221262312624126251262612627126281262912630126311263212633126341263512636126371263812639126401264112642126431264412645126461264712648126491265012651126521265312654126551265612657126581265912660126611266212663126641266512666126671266812669126701267112672126731267412675126761267712678126791268012681126821268312684126851268612687126881268912690126911269212693126941269512696126971269812699127001270112702127031270412705127061270712708127091271012711127121271312714127151271612717127181271912720127211272212723127241272512726127271272812729127301273112732127331273412735127361273712738127391274012741127421274312744127451274612747127481274912750127511275212753127541275512756127571275812759127601276112762127631276412765127661276712768127691277012771127721277312774127751277612777127781277912780127811278212783127841278512786127871278812789127901279112792127931279412795127961279712798127991280012801128021280312804128051280612807128081280912810128111281212813128141281512816128171281812819128201282112822128231282412825128261282712828128291283012831128321283312834128351283612837128381283912840128411284212843128441284512846128471284812849128501285112852128531285412855128561285712858128591286012861128621286312864128651286612867128681286912870128711287212873128741287512876128771287812879128801288112882128831288412885128861288712888128891289012891128921289312894128951289612897128981289912900129011290212903129041290512906129071290812909129101291112912129131291412915129161291712918129191292012921129221292312924129251292612927129281292912930129311293212933129341293512936129371293812939129401294112942129431294412945129461294712948129491295012951129521295312954129551295612957129581295912960129611296212963129641296512966129671296812969129701297112972129731297412975129761297712978129791298012981129821298312984129851298612987129881298912990129911299212993129941299512996129971299812999130001300113002130031300413005130061300713008130091301013011130121301313014130151301613017130181301913020130211302213023130241302513026130271302813029130301303113032130331303413035130361303713038130391304013041130421304313044130451304613047130481304913050130511305213053130541305513056130571305813059130601306113062130631306413065130661306713068130691307013071130721307313074130751307613077130781307913080130811308213083130841308513086130871308813089130901309113092130931309413095130961309713098130991310013101131021310313104131051310613107131081310913110131111311213113131141311513116131171311813119131201312113122131231312413125131261312713128131291313013131131321313313134131351313613137131381313913140131411314213143131441314513146131471314813149131501315113152131531315413155131561315713158131591316013161131621316313164131651316613167131681316913170131711317213173131741317513176131771317813179131801318113182131831318413185131861318713188131891319013191131921319313194131951319613197131981319913200132011320213203132041320513206132071320813209132101321113212132131321413215132161321713218132191322013221132221322313224132251322613227132281322913230132311323213233132341323513236132371323813239132401324113242132431324413245132461324713248132491325013251132521325313254132551325613257132581325913260//! The primary terminal emulation structure. This represents a single//! "terminal" containing a grid of characters and exposes various operations//! on that grid. This also maintains the scrollback buffer.const Terminal = @This();
const std = @import("std");const build_options = @import("terminal_options");const lib = @import("lib.zig");const assert = @import("../quirks.zig").inlineAssert;const testing = std.testing;const Allocator = std.mem.Allocator;const unicode = @import("../unicode/main.zig");const uucode = @import("uucode");
const ansi = @import("ansi.zig");const modespkg = @import("modes.zig");const charsets = @import("charsets.zig");const csi = @import("csi.zig");const hyperlink = @import("hyperlink.zig");const glyph = @import("apc/glyph.zig");const kitty = @import("kitty.zig");const osc = @import("osc.zig");const point = @import("point.zig");const sgr = @import("sgr.zig");const Tabstops = @import("Tabstops.zig");const color = @import("color.zig");const mouse = @import("mouse.zig");const Stream = @import("stream_terminal.zig").Stream;
const size = @import("size.zig");const pagepkg = @import("page.zig");const style = @import("style.zig");const Screen = @import("Screen.zig");const ScreenSet = @import("ScreenSet.zig");const Page = pagepkg.Page;const Cell = pagepkg.Cell;const Row = pagepkg.Row;
const log = std.log.scoped(.terminal);
/// Default tabstop intervalconst TABSTOP_INTERVAL = 8;
/// The set of screens behind this terminal (e.g. primary vs alternate).screens: ScreenSet,
/// Whether we're currently writing to the status line (DECSASD and DECSSDT)./// We don't support a status line currently so we just black hole this/// data so that it doesn't mess up our main display.status_display: ansi.StatusDisplay = .main,
/// Where the tabstops are.tabstops: Tabstops,
/// The size of the terminal.rows: size.CellCountInt,cols: size.CellCountInt,
/// The size of the screen in pixels. This is used for pty events and imageswidth_px: u32 = 0,height_px: u32 = 0,
/// The current scrolling region.scrolling_region: ScrollingRegion,
/// The last reported pwd, if any.pwd: std.ArrayList(u8),
/// The title of the terminal as set by escape sequences (e.g. OSC 0/2).title: std.ArrayList(u8),
/// The color state for this terminal.colors: Colors,
/// The previous printed character. This is used for the repeat previous/// char CSI (ESC [ <n> b).previous_char: ?u21 = null,
/// The modes that this terminal currently has active.modes: modespkg.ModeState = .{},
/// The most recently set mouse shape for the terminal.mouse_shape: mouse.Shape = .text,
/// Per-session Glyph Protocol registrations.glyph_glossary: glyph.Glossary = .empty,
/// These are just a packed set of flags we may set on the terminal.flags: packed struct { // This supports a Kitty extension where programs using semantic // prompts (OSC133) can annotate their new prompts with `redraw=0` to // disable clearing the prompt on resize. shell_redraws_prompt: osc.semantic_prompt.Redraw = .true,
// This is set via ESC[4;2m. Any other modify key mode just sets // this to false and we act in mode 1 by default. modify_other_keys_2: bool = false,
/// The mouse event mode and format. These are set to the last /// set mode in modes. You can't get the right event/format to use /// based on modes alone because modes don't show you what order /// this was called so we have to track it separately. mouse_event: mouse.Event = .none, mouse_format: mouse.Format = .x10,
/// Set via the XTSHIFTESCAPE sequence. If true (XTSHIFTESCAPE = 1) /// then we want to capture the shift key for the mouse protocol /// if the configuration allows it. mouse_shift_capture: enum(u2) { null, false, true } = .null,
/// True if the window is focused. focused: bool = true,
/// True if the terminal is in a password entry mode. This is set /// to true based on termios state. This is set /// to true based on termios state. password_input: bool = false,
/// True if the terminal should perform selection scrolling. selection_scroll: bool = false,
/// Dirty flag used only by the search thread. The renderer is expected /// to set this to true if the viewport was dirty as it was rendering. /// This is used by the search thread to more efficiently re-search the /// viewport and active area. /// /// Since the renderer is going to inspect the viewport/active area ANYWAYS, /// this lets our search thread do less work and hold the lock less time, /// resulting in more throughput for everything. search_viewport_dirty: bool = false,
/// Dirty flags for the renderer. dirty: Dirty = .{},} = .{},
/// The various color configurations a terminal maintains and that can/// be set dynamically via OSC, with defaults usually coming from a/// configuration.pub const Colors = struct { background: color.DynamicRGB, foreground: color.DynamicRGB, cursor: color.DynamicRGB, palette: color.DynamicPalette,
pub const default: Colors = .{ .background = .unset, .foreground = .unset, .cursor = .unset, .palette = .default, };};
/// This is a set of dirty flags the renderer can use to determine/// what parts of the screen need to be redrawn. It is up to the renderer/// to clear these flags.////// This only contains dirty flags for terminal state, not for the screen/// state. The screen state has its own dirty flags.pub const Dirty = packed struct { /// Set when the color palette is modified in any way. palette: bool = false,
/// Set when the reverse colors mode is modified. reverse_colors: bool = false,
/// Screen clear of some kind. This can be due to a screen change, /// erase display, etc. clear: bool = false,
/// Set when the pre-edit is modified. preedit: bool = false,
/// Set when Glyph Protocol registrations may have changed. Registered /// glyphs can affect already-visible PUA cells, so this requires a full /// render-state rebuild. glyph_glossary: bool = false,};
/// Scrolling region is the area of the screen designated where scrolling/// occurs. When scrolling the screen, only this viewport is scrolled.pub const ScrollingRegion = struct { // Top and bottom of the scroll region (0-indexed) // Precondition: top < bottom top: size.CellCountInt, bottom: size.CellCountInt,
// Left/right scroll regions. // Precondition: right > left // Precondition: right <= cols - 1 left: size.CellCountInt, right: size.CellCountInt,};
pub const Options = struct { cols: size.CellCountInt, rows: size.CellCountInt, max_scrollback: usize = 10_000, colors: Colors = .default,
/// The default mode state. When the terminal gets a reset, it /// will revert back to this state. default_modes: modespkg.ModePacked = .{},
/// The total storage limit for Kitty images in bytes. Has no effect /// if kitty images are disabled at build-time. kitty_image_storage_limit: usize = switch (build_options.artifact) { .ghostty => 320 * 1000 * 1000, // 320MB
// libghostty we start with a much lower limit since this is an // embedded library and we want to be more conservative with memory // usage by default. .lib => 10 * 1000 * 1000, // 10MB },
/// The limits for what medium types are allowed for Kitty image loading. /// Has no effect if kitty images are disabled otherwise. For example, // if no `sys.decode_png` hook is specified, png formats are disabled // no matter what. kitty_image_loading_limits: if (build_options.kitty_graphics) kitty.graphics.LoadingImage.Limits else void = if (build_options.kitty_graphics) .direct else {},};
/// Initialize a new terminal.pub fn init( alloc: Allocator, opts: Options,) !Terminal { const cols = opts.cols; const rows = opts.rows;
var screen_set: ScreenSet = try .init(alloc, .{ .cols = cols, .rows = rows, .max_scrollback = opts.max_scrollback, .kitty_image_storage_limit = opts.kitty_image_storage_limit, .kitty_image_loading_limits = opts.kitty_image_loading_limits, }); errdefer screen_set.deinit(alloc);
return .{ .cols = cols, .rows = rows, .screens = screen_set, .tabstops = try .init(alloc, cols, TABSTOP_INTERVAL), .scrolling_region = .{ .top = 0, .bottom = rows - 1, .left = 0, .right = cols - 1, }, .pwd = .empty, .title = .empty, .colors = opts.colors, .modes = .{ .values = opts.default_modes, .default = opts.default_modes, }, };}
pub fn deinit(self: *Terminal, alloc: Allocator) void { self.tabstops.deinit(alloc); self.screens.deinit(alloc); self.pwd.deinit(alloc); self.title.deinit(alloc); self.glyph_glossary.deinit(alloc); self.* = undefined;}
/// Return a terminal.Stream that can process VT streams and update this/// terminal state. The streams will only process read-only data that/// modifies terminal state.////// Sequences that query or otherwise require output will be ignored./// If you want to handle side effects, use `vtHandler` and set the/// effects field yourself, then initialize a stream.////// This must be deinitialized by the caller.////// Important: this creates a new stream each time with fresh parser state./// If you need to persist parser state across multiple writes (e.g./// for handling escape sequences split across write boundaries), you/// must store and reuse the returned stream.pub fn vtStream(self: *Terminal) Stream { return .initAlloc(self.gpa(), self.vtHandler());}
/// This is the handler-side only for vtStream.pub fn vtHandler(self: *Terminal) Stream.Handler { return .init(self);}
/// The general allocator we should use for this terminal.pub fn gpa(self: *Terminal) Allocator { return self.screens.active.alloc;}
/// Print UTF-8 encoded string to the terminal.pub fn printString(self: *Terminal, str: []const u8) !void { const view = try std.unicode.Utf8View.init(str); var it = view.iterator(); while (it.nextCodepoint()) |cp| { switch (cp) { '\n' => { self.carriageReturn(); try self.linefeed(); },
else => try self.print(cp), } }}
/// Print the previous printed character a repeated amount of times.pub fn printRepeat(self: *Terminal, count_req: usize) !void { if (self.previous_char) |c| { const count = @max(count_req, 1); for (0..count) |_| try self.print(c); }}
pub fn print(self: *Terminal, c: u21) !void { // log.debug("print={x} y={} x={}", .{ c, self.screens.active.cursor.y, self.screens.active.cursor.x });
// If we're not on the main display, do nothing for now if (self.status_display != .main) { @branchHint(.cold); return; }
// After doing any printing, wrapping, scrolling, etc. we want to ensure // that our screen remains in a consistent state. defer self.screens.active.assertIntegrity();
// Our right margin depends where our cursor is now. const right_limit = if (self.screens.active.cursor.x > self.scrolling_region.right) self.cols else self.scrolling_region.right + 1;
// Perform grapheme clustering if grapheme support is enabled (mode 2027). // This is MUCH slower than the normal path so the conditional below is // purposely ordered in least-likely to most-likely so we can drop out // as quickly as possible. if (c > 255 and self.modes.get(.grapheme_cluster) and self.screens.active.cursor.x > 0) grapheme: { @branchHint(.unlikely); // We need the previous cell to determine if we're at a grapheme // break or not. If we are NOT, then we are still combining the // same grapheme, and will be appending to prev.cell. Otherwise, we are // in a new cell. const Prev = struct { cell: *Cell, left: size.CellCountInt }; var prev: Prev = prev: { const left: size.CellCountInt = left: { // If we have wraparound, then we use the prev col unless // there's a pending wrap, in which case we use the current. if (self.modes.get(.wraparound)) { break :left @intFromBool(!self.screens.active.cursor.pending_wrap); }
// If we do not have wraparound, the logic is trickier. If // we're not on the last column, then we just use the previous // column. Otherwise, we need to check if there is text to // figure out if we're attaching to the prev or current. if (self.screens.active.cursor.x != right_limit - 1) break :left 1; break :left @intFromBool(self.screens.active.cursor.page_cell.codepoint() == 0); };
// If the previous cell is a wide spacer tail, then we actually // want to use the cell before that because that has the actual // content. const immediate = self.screens.active.cursorCellLeft(left); break :prev switch (immediate.wide) { else => .{ .cell = immediate, .left = left }, .spacer_tail => .{ .cell = self.screens.active.cursorCellLeft(left + 1), .left = left + 1, }, }; };
// If our cell has no content, then this is a new cell and // necessarily a grapheme break. if (prev.cell.codepoint() == 0) break :grapheme;
var previous_codepoint: u21 = prev.cell.content.codepoint; const grapheme_break = brk: { var state: uucode.grapheme.BreakState = .default; if (prev.cell.hasGrapheme()) { const cps = self.screens.active.cursor.page_pin.node.data.lookupGrapheme(prev.cell).?; for (cps) |cp2| { // log.debug("cp1={x} cp2={x}", .{ previous_codepoint, cp2 }); assert(!unicode.graphemeBreak(previous_codepoint, cp2, &state)); previous_codepoint = cp2; } }
// log.debug("cp1={x} cp2={x} end", .{ previous_codepoint, c }); break :brk unicode.graphemeBreak(previous_codepoint, c, &state); };
// If we can NOT break, this means that "c" is part of a grapheme // with the previous char. if (!grapheme_break) { var desired_wide: enum { no_change, wide, narrow } = .no_change;
// If this is an emoji variation selector then we need to modify // the cell width accordingly. VS16 makes the character wide and // VS15 makes it narrow. if (c == 0xFE0F or c == 0xFE0E) { const prev_props = unicode.table.get(previous_codepoint); // Check if it is a valid variation sequence in // emoji-variation-sequences.txt, and if not, ignore the char. if (!prev_props.emoji_vs_base) return;
switch (c) { 0xFE0F => desired_wide = .wide, 0xFE0E => desired_wide = .narrow, else => unreachable, } } else if (!unicode.table.get(c).width_zero_in_grapheme) { // If we have a code point that contributes to the width of a // grapheme, it necessarily means that we're at least at width // 2, since the first code point must be at least width 1 to // start. (Note that Prepend code points could effectively mean // the first code point should be width 0, but we don't handle // that yet.) desired_wide = .wide; }
switch (desired_wide) { .wide => wide: { if (prev.cell.wide == .wide) break :wide;
// Move our cursor back to the previous. We'll move // the cursor within this block to the proper location. self.screens.active.cursorLeft(prev.left);
// If we don't have space for the wide char, we need to // insert spacers and wrap. We need special handling if the // previous cell has grapheme data. if (self.screens.active.cursor.x == right_limit - 1) { if (!self.modes.get(.wraparound)) return;
// This path can write a spacer_head before printWrap // which can trigger integrity violations so mark // the wrap first to keep the intermediary state valid // if we're wrapping. const row_wrap = right_limit == self.cols; if (row_wrap) self.screens.active.cursor.page_row.wrap = true;
const prev_cp = prev.cell.content.codepoint; if (prev.cell.hasGrapheme()) { // This is like printCell but without clearing the // grapheme data from the cell, so we can move it // later. prev.cell.wide = if (row_wrap) .spacer_head else .narrow; prev.cell.content.codepoint = 0;
try self.printWrap(); self.printCell(prev_cp, .wide);
const new_pin = self.screens.active.cursor.page_pin.*; const new_rac = new_pin.rowAndCell();
transfer_graphemes: { var old_pin = self.screens.active.cursor.page_pin.up(1) orelse break :transfer_graphemes; old_pin.x = right_limit - 1; const old_rac = old_pin.rowAndCell();
if (new_pin.node == old_pin.node) { new_pin.node.data.moveGrapheme(prev.cell, new_rac.cell); prev.cell.content_tag = .codepoint; new_rac.cell.content_tag = .codepoint_grapheme; new_rac.row.grapheme = true; } else { const cps = old_pin.node.data.lookupGrapheme(old_rac.cell).?; for (cps) |cp| { try self.screens.active.appendGrapheme(new_rac.cell, cp); } old_pin.node.data.clearGrapheme(old_rac.cell); }
old_pin.node.data.updateRowGraphemeFlag(old_rac.row); }
// Point prev.cell to our new previous cell that // we'll be appending graphemes to prev.cell = new_rac.cell; } else { self.printCell( 0, if (row_wrap) .spacer_head else .narrow, ); try self.printWrap(); self.printCell(prev_cp, .wide);
// Point prev.cell to our new previous cell that // we'll be appending graphemes to prev.cell = self.screens.active.cursor.page_cell; } } else { prev.cell.wide = .wide; }
// Write our spacer, since prev.cell is now wide self.screens.active.cursorRight(1); self.printCell(0, .spacer_tail);
// Move the cursor again so we're beyond our spacer if (self.screens.active.cursor.x == right_limit - 1) { self.screens.active.cursor.pending_wrap = true; } else { self.screens.active.cursorRight(1); } },
.narrow => narrow: { // Prev cell is no longer wide if (prev.cell.wide != .wide) break :narrow; prev.cell.wide = .narrow;
// Remove the wide spacer tail const cell = self.screens.active.cursorCellLeft(prev.left - 1); cell.wide = .narrow;
// Back track the cursor so that we don't end up with // an extra space after the character. Since xterm is // not VS aware, it cannot be used as a reference for // this behavior; but it does follow the principle of // least surprise, and also matches the behavior that // can be observed in Kitty, which is one of the only // other VS aware terminals. if (self.screens.active.cursor.x == right_limit - 1) { // If we're already at the right edge, we stay // here and set the pending wrap to false since // when we pend a wrap, we only move our cursor once // even for wide chars (tests verify). self.screens.active.cursor.pending_wrap = false; } else { // Otherwise, move back. self.screens.active.cursorLeft(1); }
break :narrow; },
else => {}, }
log.debug("c={X} grapheme attach to left={} primary_cp={X}", .{ c, prev.left, prev.cell.codepoint(), }); self.screens.active.cursorMarkDirty(); try self.screens.active.appendGrapheme(prev.cell, c); return; } }
// Determine the width of this character so we can handle // non-single-width characters properly. We have a fast-path for // byte-sized characters since they're so common. We can ignore // control characters because they're always filtered prior. const width: usize = if (c <= 0xFF) 1 else @intCast(unicode.table.get(c).width);
// Note: it is possible to have a width of "3" and a width of "-1" from // uucode.x's wcwidth. We should look into those cases and handle them // appropriately. assert(width <= 2); // log.debug("c={x} width={}", .{ c, width });
// Attach zero-width characters to our cell as grapheme data. if (width == 0) { @branchHint(.unlikely); // If we have grapheme clustering enabled, we don't blindly attach // any zero width character to our cells and we instead just ignore // it. if (self.modes.get(.grapheme_cluster)) return;
// If we have wraparound enabled and a pending wrap, the character // we're attaching to is still under the cursor. Otherwise, it's the // cell to the left. const left: size.CellCountInt = if (self.modes.get(.wraparound) and self.screens.active.cursor.pending_wrap) 0 else 1;
// If we're at cell zero and not pending a wrap, then this is malformed // data and we don't print anything or even store this. Zero-width // characters are ALWAYS attached to some other non-zero-width // character at the time of writing. if (self.screens.active.cursor.x == 0 and left == 1) { log.warn("zero-width character with no prior character, ignoring", .{}); return; }
// Find our previous cell const prev = prev: { const immediate = self.screens.active.cursorCellLeft(left); if (immediate.wide != .spacer_tail) break :prev immediate; break :prev self.screens.active.cursorCellLeft(left + 1); };
// If our previous cell has no text, just ignore the zero-width character if (!prev.hasText()) { log.warn("zero-width character with no prior character, ignoring", .{}); return; }
// If this is a emoji variation selector, prev must be an emoji if (c == 0xFE0F or c == 0xFE0E) { const prev_props = unicode.table.get(prev.content.codepoint); const emoji = prev_props.grapheme_break == .extended_pictographic; if (!emoji) return; }
try self.screens.active.appendGrapheme(prev, c); return; }
// We have a printable character, save it self.previous_char = c;
// If we're soft-wrapping, then handle that first. if (self.screens.active.cursor.pending_wrap and self.modes.get(.wraparound)) { try self.printWrap(); }
// If we have insert mode enabled then we need to handle that. We // only do insert mode if we're not at the end of the line. if (self.modes.get(.insert) and self.screens.active.cursor.x + width < self.cols) { self.insertBlanks(width); }
switch (width) { // Single cell is very easy: just write in the cell 1 => { @branchHint(.likely); self.screens.active.cursorMarkDirty(); @call(.always_inline, printCell, .{ self, c, .narrow }); },
// Wide character requires a spacer. We print this by // using two cells: the first is flagged "wide" and has the // wide char. The second is guaranteed to be a spacer if // we're not at the end of the line. 2 => if ((right_limit - self.scrolling_region.left) > 1) { // If we don't have space for the wide char, we need // to insert spacers and wrap. Then we just print the wide // char as normal. if (self.screens.active.cursor.x == right_limit - 1) { // If we don't have wraparound enabled then we don't print // this character at all and don't move the cursor. This is // how xterm behaves. if (!self.modes.get(.wraparound)) return;
// We only create a spacer head if we're at the real edge // of the screen. Otherwise, we clear the space with a narrow. // This allows soft wrapping to work correctly. if (right_limit == self.cols) { // Special-case: we need to set wrap to true even // though we call printWrap below because if there is // a page resize during printCell then it'll fail // integrity checks. self.screens.active.cursor.page_row.wrap = true; self.printCell(0, .spacer_head); } else { self.printCell(0, .narrow); } try self.printWrap(); }
self.screens.active.cursorMarkDirty(); self.printCell(c, .wide); self.screens.active.cursorRight(1); self.printCell(0, .spacer_tail); } else { // This is pretty broken, terminals should never be only 1-wide. // We should prevent this downstream. self.screens.active.cursorMarkDirty(); self.printCell(0, .narrow); },
else => unreachable, }
// If we're at the column limit, then we need to wrap the next time. // In this case, we don't move the cursor. if (self.screens.active.cursor.x == right_limit - 1) { self.screens.active.cursor.pending_wrap = true; return; }
// Move the cursor self.screens.active.cursorRight(1);}
fn printCell( self: *Terminal, unmapped_c: u21, wide: Cell.Wide,) void { defer self.screens.active.assertIntegrity();
// TODO: spacers should use a bgcolor only cell
const c: u21 = c: { // TODO: non-utf8 handling, gr
// If we're single shifting, then we use the key exactly once. const key = if (self.screens.active.charset.single_shift) |key_once| blk: { self.screens.active.charset.single_shift = null; break :blk key_once; } else self.screens.active.charset.gl;
const set = self.screens.active.charset.charsets.get(key);
// UTF-8 or ASCII is used as-is if (set == .utf8 or set == .ascii) { @branchHint(.likely); break :c unmapped_c; }
// If we're outside of ASCII range this is an invalid value in // this table so we just return space. if (unmapped_c > std.math.maxInt(u8)) break :c ' ';
// Get our lookup table and map it const table = charsets.table(set); break :c @intCast(table[@intCast(unmapped_c)]); };
const cell = self.screens.active.cursor.page_cell;
// If the wide property of this cell is the same, then we don't // need to do the special handling here because the structure will // be the same. If it is NOT the same, then we may need to clear some // cells. if (cell.wide != wide) { switch (cell.wide) { // Previous cell was narrow. Do nothing. .narrow => {},
// Previous cell was wide. We need to clear the tail and head. .wide => wide: { if (self.screens.active.cursor.x >= self.cols - 1) break :wide;
const spacer_cell = self.screens.active.cursorCellRight(1); self.screens.active.clearCells( &self.screens.active.cursor.page_pin.node.data, self.screens.active.cursor.page_row, spacer_cell[0..1], );
// If we're near the left edge, a wide char may have // wrapped from the previous row, leaving a spacer_head // at the end of that row. Clear it so the previous row // doesn't keep a stale spacer_head. if (self.screens.active.cursor.y > 0 and self.screens.active.cursor.x <= 1) { const head_cell = self.screens.active.cursorCellEndOfPrev(); if (head_cell.wide == .spacer_head) head_cell.wide = .narrow; } },
.spacer_tail => { assert(self.screens.active.cursor.x > 0);
// So integrity checks pass. We fix this up later so we don't // need to do this without safety checks. if (comptime std.debug.runtime_safety) { cell.wide = .narrow; }
const wide_cell = self.screens.active.cursorCellLeft(1); self.screens.active.clearCells( &self.screens.active.cursor.page_pin.node.data, self.screens.active.cursor.page_row, wide_cell[0..1], ); // If we're near the left edge, a wide char may have // wrapped from the previous row, leaving a spacer_head // at the end of that row. Clear it so the previous row // doesn't keep a stale spacer_head. if (self.screens.active.cursor.y > 0 and self.screens.active.cursor.x <= 1) { const head_cell = self.screens.active.cursorCellEndOfPrev(); if (head_cell.wide == .spacer_head) head_cell.wide = .narrow; } },
// TODO: this case was not handled in the old terminal implementation // but it feels like we should do something. investigate other // terminals (xterm mainly) and see what's up. .spacer_head => {}, } }
// If the prior value had graphemes, clear those if (cell.hasGrapheme()) { const page = &self.screens.active.cursor.page_pin.node.data; page.clearGrapheme(cell); page.updateRowGraphemeFlag(self.screens.active.cursor.page_row); }
// We don't need to update the style refs unless the // cell's new style will be different after writing. const style_changed = cell.style_id != self.screens.active.cursor.style_id; if (style_changed) { var page = &self.screens.active.cursor.page_pin.node.data;
// Release the old style. if (cell.style_id != style.default_id) { assert(self.screens.active.cursor.page_row.styled); page.styles.release(page.memory, cell.style_id); } }
// Keep track if we had a hyperlink so we can unset it. const had_hyperlink = cell.hyperlink;
// Write cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = c }, .style_id = self.screens.active.cursor.style_id, .wide = wide, .protected = self.screens.active.cursor.protected, .semantic_content = self.screens.active.cursor.semantic_content, };
if (style_changed) { var page = &self.screens.active.cursor.page_pin.node.data;
// Use the new style. if (cell.style_id != style.default_id) { page.styles.use(page.memory, cell.style_id); self.screens.active.cursor.page_row.styled = true; } }
// If this is a Kitty unicode placeholder then we need to mark the // row so that the renderer can lookup rows with these much faster. if (comptime build_options.kitty_graphics) { if (c == kitty.graphics.unicode.placeholder) { @branchHint(.unlikely); self.screens.active.cursor.page_row.kitty_virtual_placeholder = true; } }
// We check for an active hyperlink first because setHyperlink // handles clearing the old hyperlink and an optimization if we're // overwriting the same hyperlink. if (self.screens.active.cursor.hyperlink_id > 0) { self.screens.active.cursorSetHyperlink() catch |err| { @branchHint(.unlikely); log.warn("error reallocating for more hyperlink space, ignoring hyperlink err={}", .{err}); assert(!cell.hyperlink); }; } else if (had_hyperlink) { // If the previous cell had a hyperlink then we need to clear it. var page = &self.screens.active.cursor.page_pin.node.data; page.clearHyperlink(cell); page.updateRowHyperlinkFlag(self.screens.active.cursor.page_row); }}
fn printWrap(self: *Terminal) !void { // We only mark that we soft-wrapped if we're at the edge of our // full screen. We don't mark the row as wrapped if we're in the // middle due to a right margin. const cursor: *Screen.Cursor = &self.screens.active.cursor; const mark_wrap = cursor.x == self.cols - 1; if (mark_wrap) cursor.page_row.wrap = true;
// Get the old semantic prompt so we can extend it to the next // line. We need to do this before we index() because we may // modify memory. const old_semantic = cursor.semantic_content; const old_semantic_clear = cursor.semantic_content_clear_eol;
// Move to the next line try self.index(); self.screens.active.cursorHorizontalAbsolute(self.scrolling_region.left);
// Our pointer should never move assert(cursor == &self.screens.active.cursor);
// We always reset our semantic prompt state cursor.semantic_content = old_semantic; cursor.semantic_content_clear_eol = old_semantic_clear; switch (old_semantic) { .output, .input => {}, .prompt => cursor.page_row.semantic_prompt = .prompt_continuation, }
if (mark_wrap) { const row = self.screens.active.cursor.page_row; // Always mark the row as a continuation row.wrap_continuation = true; }
// Assure that our screen is consistent self.screens.active.assertIntegrity();}
/// Set the charset into the given slot.pub fn configureCharset(self: *Terminal, slot: charsets.Slots, set: charsets.Charset) void { self.screens.active.charset.charsets.set(slot, set);}
/// Invoke the charset in slot into the active slot. If single is true,/// then this will only be invoked for a single character.pub fn invokeCharset( self: *Terminal, active: charsets.ActiveSlot, slot: charsets.Slots, single: bool,) void { if (single) { assert(active == .GL); self.screens.active.charset.single_shift = slot; return; }
switch (active) { .GL => self.screens.active.charset.gl = slot, .GR => self.screens.active.charset.gr = slot, }}
/// Carriage return moves the cursor to the first column.pub fn carriageReturn(self: *Terminal) void { // Always reset pending wrap state self.screens.active.cursor.pending_wrap = false;
// In origin mode we always move to the left margin self.screens.active.cursorHorizontalAbsolute(if (self.modes.get(.origin)) self.scrolling_region.left else if (self.screens.active.cursor.x >= self.scrolling_region.left) self.scrolling_region.left else 0);}
/// Linefeed moves the cursor to the next line.pub fn linefeed(self: *Terminal) !void { try self.index(); if (self.modes.get(.linefeed)) self.carriageReturn();}
/// Backspace moves the cursor back a column (but not less than 0).pub fn backspace(self: *Terminal) void { self.cursorLeft(1);}
/// Move the cursor up amount lines. If amount is greater than the maximum/// move distance then it is internally adjusted to the maximum. If amount is/// 0, adjust it to 1.pub fn cursorUp(self: *Terminal, count_req: usize) void { // Always resets pending wrap self.screens.active.cursor.pending_wrap = false;
// The maximum amount the cursor can move up depends on scrolling regions const max = if (self.screens.active.cursor.y >= self.scrolling_region.top) self.screens.active.cursor.y - self.scrolling_region.top else self.screens.active.cursor.y; const count = @min(max, @max(count_req, 1));
// We can safely intCast below because of the min/max clamping we did above. self.screens.active.cursorUp(@intCast(count));}
/// Move the cursor down amount lines. If amount is greater than the maximum/// move distance then it is internally adjusted to the maximum. This sequence/// will not scroll the screen or scroll region. If amount is 0, adjust it to 1.pub fn cursorDown(self: *Terminal, count_req: usize) void { // Always resets pending wrap self.screens.active.cursor.pending_wrap = false;
// The max the cursor can move to depends where the cursor currently is const max = if (self.screens.active.cursor.y <= self.scrolling_region.bottom) self.scrolling_region.bottom - self.screens.active.cursor.y else self.rows - self.screens.active.cursor.y - 1; const count = @min(max, @max(count_req, 1)); self.screens.active.cursorDown(@intCast(count));}
/// Move the cursor right amount columns. If amount is greater than the/// maximum move distance then it is internally adjusted to the maximum./// This sequence will not scroll the screen or scroll region. If amount is/// 0, adjust it to 1.pub fn cursorRight(self: *Terminal, count_req: usize) void { // Always resets pending wrap self.screens.active.cursor.pending_wrap = false;
// The max the cursor can move to depends where the cursor currently is const max = if (self.screens.active.cursor.x <= self.scrolling_region.right) self.scrolling_region.right - self.screens.active.cursor.x else self.cols - self.screens.active.cursor.x - 1; const count = @min(max, @max(count_req, 1)); self.screens.active.cursorRight(@intCast(count));}
/// Move the cursor to the left amount cells. If amount is 0, adjust it to 1.pub fn cursorLeft(self: *Terminal, count_req: usize) void { // Wrapping behavior depends on various terminal modes const WrapMode = enum { none, reverse, reverse_extended }; const wrap_mode: WrapMode = wrap_mode: { if (!self.modes.get(.wraparound)) break :wrap_mode .none; if (self.modes.get(.reverse_wrap_extended)) break :wrap_mode .reverse_extended; if (self.modes.get(.reverse_wrap)) break :wrap_mode .reverse; break :wrap_mode .none; };
var count = @max(count_req, 1);
// If we are in no wrap mode, then we move the cursor left and exit // since this is the fastest and most typical path. if (wrap_mode == .none) { self.screens.active.cursorLeft(@min(count, self.screens.active.cursor.x)); self.screens.active.cursor.pending_wrap = false; return; }
// If we have a pending wrap state and we are in either reverse wrap // modes then we decrement the amount we move by one to match xterm. if (self.screens.active.cursor.pending_wrap) { count -= 1; self.screens.active.cursor.pending_wrap = false; }
// The margins we can move to. const top = self.scrolling_region.top; const bottom = self.scrolling_region.bottom; const right_margin = self.scrolling_region.right; const left_margin = if (self.screens.active.cursor.x < self.scrolling_region.left) 0 else self.scrolling_region.left;
// Handle some edge cases when our cursor is already on the left margin. if (self.screens.active.cursor.x == left_margin) { switch (wrap_mode) { // In reverse mode, if we're already before the top margin // then we just set our cursor to the top-left and we're done. .reverse => if (self.screens.active.cursor.y <= top) { self.screens.active.cursorAbsolute(left_margin, top); return; },
// Handled in while loop .reverse_extended => {},
// Handled above .none => unreachable, } }
while (true) { // We can move at most to the left margin. const max = self.screens.active.cursor.x - left_margin;
// We want to move at most the number of columns we have left // or our remaining count. Do the move. const amount = @min(max, count); count -= amount; self.screens.active.cursorLeft(amount);
// If we have no more to move, then we're done. if (count == 0) break;
// If we are at the top, then we are done. if (self.screens.active.cursor.y == top) { if (wrap_mode != .reverse_extended) break;
self.screens.active.cursorAbsolute(right_margin, bottom); count -= 1; continue; }
// UNDEFINED TERMINAL BEHAVIOR. This situation is not handled in xterm // and currently results in a crash in xterm. Given no other known // terminal [to me] implements XTREVWRAP2, I decided to just mimic // the behavior of xterm up and not including the crash by wrapping // up to the (0, 0) and stopping there. My reasoning is that for an // appropriately sized value of "count" this is the behavior that xterm // would have. This is unit tested. if (self.screens.active.cursor.y == 0) { assert(self.screens.active.cursor.x == left_margin); break; }
// If our previous line is not wrapped then we are done. if (wrap_mode != .reverse_extended) { const prev_row = self.screens.active.cursorRowUp(1); if (!prev_row.wrap) break; }
self.screens.active.cursorAbsolute(right_margin, self.screens.active.cursor.y - 1); count -= 1; }}
/// Save cursor position and further state.////// The primary and alternate screen have distinct save state. One saved state/// is kept per screen (main / alternative). If for the current screen state/// was already saved it is overwritten.pub fn saveCursor(self: *Terminal) void { self.screens.active.saved_cursor = .{ .x = self.screens.active.cursor.x, .y = self.screens.active.cursor.y, .style = self.screens.active.cursor.style, .protected = self.screens.active.cursor.protected, .pending_wrap = self.screens.active.cursor.pending_wrap, .origin = self.modes.get(.origin), .charset = self.screens.active.charset, };}
/// Restore cursor position and other state.////// The primary and alternate screen have distinct save state./// If no save was done before values are reset to their initial values.pub fn restoreCursor(self: *Terminal) void { const saved: Screen.SavedCursor = self.screens.active.saved_cursor orelse .{ .x = 0, .y = 0, .style = .{}, .protected = false, .pending_wrap = false, .origin = false, .charset = .{}, };
// Set the style first because it can fail self.screens.active.cursor.style = saved.style; self.screens.active.manualStyleUpdate() catch |err| { // Regardless of the error here, we revert back to an unstyled // cursor. It is more important that the restore succeeds in // other attributes because terminals have no way to communicate // failure back. log.warn("restoreCursor error updating style err={}", .{err}); const screen: *Screen = self.screens.active; screen.cursor.style = .{}; self.screens.active.manualStyleUpdate() catch unreachable; };
self.screens.active.charset = saved.charset; self.modes.set(.origin, saved.origin); self.screens.active.cursor.pending_wrap = saved.pending_wrap; self.screens.active.cursor.protected = saved.protected; self.screens.active.cursorAbsolute( @min(saved.x, self.cols - 1), @min(saved.y, self.rows - 1), );
// Ensure our screen is consistent self.screens.active.assertIntegrity();}
/// Set the character protection mode for the terminal.pub fn setProtectedMode(self: *Terminal, mode: ansi.ProtectedMode) void { switch (mode) { .off => { self.screens.active.cursor.protected = false;
// screen.protected_mode is NEVER reset to ".off" because // logic such as eraseChars depends on knowing what the // _most recent_ mode was. },
.iso => { self.screens.active.cursor.protected = true; self.screens.active.protected_mode = .iso; },
.dec => { self.screens.active.cursor.protected = true; self.screens.active.protected_mode = .dec; }, }}
/// Perform a semantic prompt command.////// If there is an error, we do our best to get the terminal into/// some coherent state, since callers typically can't handle errors/// (since they're sending sequences via the pty).pub fn semanticPrompt( self: *Terminal, cmd: osc.Command.SemanticPrompt,) !void { switch (cmd.action) { .fresh_line => try self.semanticPromptFreshLine(),
.fresh_line_new_prompt => { // "First do a fresh-line." try self.semanticPromptFreshLine();
const screen: *Screen = self.screens.active;
// "Subsequent text (until a OSC "133;B" or OSC "133;I" command) // is a prompt string (as if followed by OSC 133;P;k=i\007)." screen.cursorSetSemanticContent(.{ .prompt = cmd.readOption(.prompt_kind) orelse .initial, });
// This is a kitty-specific flag that notes that the shell // is NOT capable of redraw. Redraw defaults to true so this // usually just disables it, but either is possible. if (cmd.readOption(.redraw)) |v| { self.flags.shell_redraws_prompt = v; }
click: { // Handle click_events as a priority over cl. click_events // is another Kitty-specific extension that converts clicks // within a prompt area to SGR mouse events and defers to the // shell to handle them. if (cmd.readOption(.click_events)) |v| { screen.semantic_prompt.click = .{ .click_events = v }; break :click; }
// If click_events was not set or disabled, fallback to `cl`. if (cmd.readOption(.cl)) |v| { screen.semantic_prompt.click = .{ .cl = v }; } }
// The "aid" and "cl" options are also valid for this // command but we don't yet handle these in any meaningful way. },
.new_command => { // Spec: // Same as OSC "133;A" but may first implicitly terminate a // previous command: if the options specify an aid and there // is an active (open) command with matching aid, finish the // innermost such command (as well as any other commands // nested more deeply). If no aid is specified, treat as an // aid whose value is the empty string.
// Ghostty: // We don't currently do explicit command tracking in any way // so there is no need to terminate prior commands. We just // perform the `A` action. try self.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = cmd.options_unvalidated, }); },
.prompt_start => { // Explicit start of prompt. Optional after an A or N command. // The k (kind) option specifies the type of prompt: // regular primary prompt (k=i or default), // right-side prompts (k=r), or prompts for continuation lines (k=c or k=s). self.screens.active.cursorSetSemanticContent(.{ .prompt = cmd.readOption(.prompt_kind) orelse .initial, }); },
.end_prompt_start_input => { // End of prompt and start of user input, terminated by a OSC // "133;C" or another prompt (OSC "133;P"). self.screens.active.cursorSetSemanticContent(.{ .input = .clear_explicit, }); },
.end_prompt_start_input_terminate_eol => { // End of prompt and start of user input, terminated by end-of-line. self.screens.active.cursorSetSemanticContent(.{ .input = .clear_eol, }); },
.end_input_start_output => { // "End of input, and start of output." self.screens.active.cursorSetSemanticContent(.output);
// If our current row is marked as a prompt and we're // at column zero then we assume we're un-prompting. This // is a heuristic to deal with fish, mostly. The issue that // fish brings up is that it has no PS2 equivalent and its // builtin OSC133 marking doesn't output continuation lines // as k=s. So, we assume when we get a newline with a prompt // cursor that the new line is also a prompt. But fish changes // to output on the newline. So if we're at col 0 we just assume // we're overwriting the prompt. if (self.screens.active.cursor.page_row.semantic_prompt != .none and self.screens.active.cursor.x == 0) { self.screens.active.cursor.page_row.semantic_prompt = .none; } },
.end_command => { // From a terminal state perspective, this doesn't really do // anything. Other terminals appear to do nothing here. I think // its reasonable at this point to reset our semantic content // state but the spec doesn't really say what to do. self.screens.active.cursorSetSemanticContent(.output); }, }}
// OSC 133;Lfn semanticPromptFreshLine(self: *Terminal) !void { const left_margin = if (self.screens.active.cursor.x < self.scrolling_region.left) 0 else self.scrolling_region.left;
// Spec: "If the cursor is the initial column (left, assuming // left-to-right writing), do nothing" This specification is very under // specified. We are taking the liberty to assume that in a left/right // margin context, if the cursor is outside of the left margin, we treat // it as being at the left margin for the purposes of this command. // This is arbitrary. If someone has a better reasonable idea we can // apply it. if (self.screens.active.cursor.x == left_margin) return;
self.carriageReturn(); try self.index();}
/// The semantic prompt type. This is used when tracking a line type and/// requires integration with the shell. By default, we mark a line as "none"/// meaning we don't know what type it is.////// See: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.mdpub const SemanticPrompt = enum { prompt, prompt_continuation, input, command,};
/// Returns true if the cursor is currently at a prompt. Another way to look/// at this is it returns false if the shell is currently outputting something./// This requires shell integration (semantic prompt integration).////// If the shell integration doesn't exist, this will always return false.pub fn cursorIsAtPrompt(self: *Terminal) bool { // If we're on the secondary screen, we're never at a prompt. if (self.screens.active_key == .alternate) return false;
// If our page row is a prompt then we're always at a prompt const cursor: *const Screen.Cursor = &self.screens.active.cursor; if (cursor.page_row.semantic_prompt != .none) return true;
// Otherwise, determine our cursor state return switch (cursor.semantic_content) { .input, .prompt => true, .output => false, };}
/// Horizontal tab moves the cursor to the next tabstop, clearing/// the screen to the left the tabstop.pub fn horizontalTab(self: *Terminal) void { while (self.screens.active.cursor.x < self.scrolling_region.right) { // Move the cursor right self.screens.active.cursorRight(1);
// If the last cursor position was a tabstop we return. We do // "last cursor position" because we want a space to be written // at the tabstop unless we're at the end (the while condition). if (self.tabstops.get(self.screens.active.cursor.x)) return; }}
// Same as horizontalTab but moves to the previous tabstop instead of the next.pub fn horizontalTabBack(self: *Terminal) void { // With origin mode enabled, our leftmost limit is the left margin. const left_limit = if (self.modes.get(.origin)) self.scrolling_region.left else 0;
while (true) { // If we're already at the edge of the screen, then we're done. if (self.screens.active.cursor.x <= left_limit) return;
// Move the cursor left self.screens.active.cursorLeft(1); if (self.tabstops.get(self.screens.active.cursor.x)) return; }}
/// Clear tab stops.pub fn tabClear(self: *Terminal, cmd: csi.TabClear) void { switch (cmd) { .current => self.tabstops.unset(self.screens.active.cursor.x), .all => self.tabstops.reset(0), else => log.warn("invalid or unknown tab clear setting: {}", .{cmd}), }}
/// Set a tab stop on the current cursor./// TODO: testpub fn tabSet(self: *Terminal) void { self.tabstops.set(self.screens.active.cursor.x);}
/// TODO: testpub fn tabReset(self: *Terminal) void { self.tabstops.reset(TABSTOP_INTERVAL);}
/// Move the cursor to the next line in the scrolling region, possibly scrolling.////// If the cursor is outside of the scrolling region: move the cursor one line/// down if it is not on the bottom-most line of the screen.////// If the cursor is inside the scrolling region:/// If the cursor is on the bottom-most line of the scrolling region:/// invoke scroll up with amount=1/// If the cursor is not on the bottom-most line of the scrolling region:/// move the cursor one line down////// This unsets the pending wrap state without wrapping.pub fn index(self: *Terminal) !void { const screen: *Screen = self.screens.active;
// Unset pending wrap state screen.cursor.pending_wrap = false;
// We handle our cursor semantic prompt state AFTER doing the // scrolling, because we may need to apply to new rows. defer if (screen.cursor.semantic_content != .output) { @branchHint(.unlikely);
// Always reset any semantic content clear-eol state. // // The specification is not clear what "end-of-line" means. If we // discover that there are more scenarios we should be unsetting // this we should document and test it. if (screen.cursor.semantic_content_clear_eol) { screen.cursor.semantic_content = .output; screen.cursor.semantic_content_clear_eol = false; } else { // If we aren't clearing our state at EOL and we're not output, // then we mark the new row as a prompt continuation. This is // to work around shells that don't send OSC 133 k=s sequences // for continuations. // // This can be a false positive if the shell changes content // type later and outputs something. We handle that in the // semanticPrompt function. screen.cursor.page_row.semantic_prompt = .prompt_continuation; } } else { // This should never be set in the output mode. assert(!screen.cursor.semantic_content_clear_eol); };
// Outside of the scroll region we move the cursor one line down. if (screen.cursor.y < self.scrolling_region.top or screen.cursor.y > self.scrolling_region.bottom) { // We only move down if we're not already at the bottom of // the screen. if (screen.cursor.y < self.rows - 1) { screen.cursorDown(1); }
return; }
// If the cursor is inside the scrolling region and on the bottom-most // line, then we scroll up. If our scrolling region is the full screen // we create scrollback. if (screen.cursor.y == self.scrolling_region.bottom and screen.cursor.x >= self.scrolling_region.left and screen.cursor.x <= self.scrolling_region.right) { if (comptime build_options.kitty_graphics) { // Scrolling dirties the images because it updates their placements pins. screen.kitty_images.dirty = true; }
// If our scrolling region is at the top, we create scrollback. if (self.scrolling_region.top == 0 and self.scrolling_region.left == 0 and self.scrolling_region.right == self.cols - 1) { try screen.cursorScrollAbove(); return; }
// Slow path for left and right scrolling region margins. if (self.scrolling_region.left != 0 or self.scrolling_region.right != self.cols - 1 or
// PERF(mitchellh): If we have an SGR background set then // we need to preserve that background in our erased rows. // scrollUp does that but eraseRowBounded below does not. // However, scrollUp is WAY slower. We should optimize this // case to work in the eraseRowBounded codepath and remove // this check. !screen.blankCell().isZero()) { try self.scrollUp(1); return; }
// Otherwise use a fast path function from PageList to efficiently // scroll the contents of the scrolling region.
// Preserve old cursor just for assertions const old_cursor = screen.cursor;
try screen.pages.eraseRowBounded( .{ .active = .{ .y = self.scrolling_region.top } }, self.scrolling_region.bottom - self.scrolling_region.top, );
// eraseRow and eraseRowBounded will end up moving the cursor pin // up by 1, so we need to move it back down. A `cursorReload` // would be better option but this is more efficient and this is // a super hot path so we do this instead. assert(screen.cursor.x == old_cursor.x); assert(screen.cursor.y == old_cursor.y); screen.cursor.y -= 1; screen.cursorDown(1);
// The operations above can prune our cursor style so we need to // update. This should never fail because the above can only FREE // memory. screen.manualStyleUpdate() catch |err| { std.log.warn("deleteLines manualStyleUpdate err={}", .{err}); screen.cursor.style = .{}; screen.manualStyleUpdate() catch unreachable; };
return; }
// Increase cursor by 1, maximum to bottom of scroll region if (screen.cursor.y < self.scrolling_region.bottom) { screen.cursorDown(1); }}
/// Move the cursor to the previous line in the scrolling region, possibly/// scrolling.////// If the cursor is outside of the scrolling region, move the cursor one/// line up if it is not on the top-most line of the screen.////// If the cursor is inside the scrolling region:////// * If the cursor is on the top-most line of the scrolling region:/// invoke scroll down with amount=1/// * If the cursor is not on the top-most line of the scrolling region:/// move the cursor one line uppub fn reverseIndex(self: *Terminal) void { if (self.screens.active.cursor.y != self.scrolling_region.top or self.screens.active.cursor.x < self.scrolling_region.left or self.screens.active.cursor.x > self.scrolling_region.right) { self.cursorUp(1); return; }
self.scrollDown(1);}
/// Set Cursor Position. Move cursor to the position indicated/// by row and column (1-indexed). If column is 0, it is adjusted to 1./// If column is greater than the right-most column it is adjusted to/// the right-most column. If row is 0, it is adjusted to 1. If row is/// greater than the bottom-most row it is adjusted to the bottom-most/// row.pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void { // If cursor origin mode is set the cursor row will be moved relative to // the top margin row and adjusted to be above or at bottom-most row in // the current scroll region. // // If origin mode is set and left and right margin mode is set the cursor // will be moved relative to the left margin column and adjusted to be on // or left of the right margin column. const params: struct { x_offset: size.CellCountInt = 0, y_offset: size.CellCountInt = 0, x_max: size.CellCountInt, y_max: size.CellCountInt, } = if (self.modes.get(.origin)) .{ .x_offset = self.scrolling_region.left, .y_offset = self.scrolling_region.top, .x_max = self.scrolling_region.right + 1, // We need this 1-indexed .y_max = self.scrolling_region.bottom + 1, // We need this 1-indexed } else .{ .x_max = self.cols, .y_max = self.rows, };
// Unset pending wrap state self.screens.active.cursor.pending_wrap = false;
// Calculate our new x/y const row = if (row_req == 0) 1 else row_req; const col = if (col_req == 0) 1 else col_req; const x = @min(params.x_max, col + params.x_offset) -| 1; const y = @min(params.y_max, row + params.y_offset) -| 1;
// If the y is unchanged then this is fast pointer math if (y == self.screens.active.cursor.y) { if (x > self.screens.active.cursor.x) { self.screens.active.cursorRight(x - self.screens.active.cursor.x); } else { self.screens.active.cursorLeft(self.screens.active.cursor.x - x); }
return; }
// If everything changed we do an absolute change which is slightly slower self.screens.active.cursorAbsolute(x, y); // log.info("set cursor position: col={} row={}", .{ self.screens.active.cursor.x, self.screens.active.cursor.y });}
/// Set Top and Bottom Margins If bottom is not specified, 0 or bigger than/// the number of the bottom-most row, it is adjusted to the number of the/// bottom most row.////// If top < bottom set the top and bottom row of the scroll region according/// to top and bottom and move the cursor to the top-left cell of the display/// (when in cursor origin mode is set to the top-left cell of the scroll region).////// Otherwise: Set the top and bottom row of the scroll region to the top-most/// and bottom-most line of the screen.////// Top and bottom are 1-indexed.pub fn setTopAndBottomMargin(self: *Terminal, top_req: usize, bottom_req: usize) void { const top = @max(1, top_req); const bottom = @min(self.rows, if (bottom_req == 0) self.rows else bottom_req); if (top >= bottom) return;
self.scrolling_region.top = @intCast(top - 1); self.scrolling_region.bottom = @intCast(bottom - 1); self.setCursorPos(1, 1);}
/// DECSLRMpub fn setLeftAndRightMargin(self: *Terminal, left_req: usize, right_req: usize) void { // We must have this mode enabled to do anything if (!self.modes.get(.enable_left_and_right_margin)) return;
const left = @max(1, left_req); const right = @min(self.cols, if (right_req == 0) self.cols else right_req); if (left >= right) return;
self.scrolling_region.left = @intCast(left - 1); self.scrolling_region.right = @intCast(right - 1); self.setCursorPos(1, 1);}
/// Scroll the text down by one row.pub fn scrollDown(self: *Terminal, count: usize) void { // Preserve our x/y to restore. const old_x = self.screens.active.cursor.x; const old_y = self.screens.active.cursor.y; const old_wrap = self.screens.active.cursor.pending_wrap; defer { self.screens.active.cursorAbsolute(old_x, old_y); self.screens.active.cursor.pending_wrap = old_wrap; }
// Move to the top of the scroll region self.screens.active.cursorAbsolute(self.scrolling_region.left, self.scrolling_region.top); self.insertLines(count);}
/// Removes amount lines from the top of the scroll region. The remaining lines/// to the bottom margin are shifted up and space from the bottom margin up/// is filled with empty lines.////// The new lines are created according to the current SGR state.////// Does not change the (absolute) cursor position.pub fn scrollUp(self: *Terminal, count: usize) !void { // Preserve our x/y to restore. const old_x = self.screens.active.cursor.x; const old_y = self.screens.active.cursor.y; const old_wrap = self.screens.active.cursor.pending_wrap; defer { self.screens.active.cursorAbsolute(old_x, old_y); self.screens.active.cursor.pending_wrap = old_wrap; }
// If our scroll region is at the top and we have no left/right // margins then we move the scrolled out text into the scrollback. if (self.scrolling_region.top == 0 and self.scrolling_region.left == 0 and self.scrolling_region.right == self.cols - 1) { // Scrolling dirties the images because it updates their placements pins. if (comptime build_options.kitty_graphics) { self.screens.active.kitty_images.dirty = true; }
// Clamp count to the scroll region height. const region_height = self.scrolling_region.bottom + 1; const adjusted_count = @min(count, region_height);
// TODO: Create an optimized version that can scroll N times // This isn't critical because in most cases, scrollUp is used // with count=1, but it's still a big optimization opportunity.
// Move our cursor to the bottom of the scroll region so we can // use the cursorScrollAbove function to create scrollback self.screens.active.cursorAbsolute(0, self.scrolling_region.bottom); for (0..adjusted_count) |_| try self.screens.active.cursorScrollAbove(); return; }
// Move to the top of the scroll region self.screens.active.cursorAbsolute(self.scrolling_region.left, self.scrolling_region.top); self.deleteLines(count);}
/// Options for scrolling the viewport of the terminal grid.pub const ScrollViewport = union(Tag) { /// Scroll to the top of the scrollback top,
/// Scroll to the bottom, i.e. the top of the active area bottom,
/// Scroll by some delta amount, up is negative. delta: isize,
pub const Tag = lib.Enum(lib.target, &.{ "top", "bottom", "delta", });
const c_union = lib.TaggedUnion( lib.target, @This(), // Padding: largest variant is isize (8 bytes on 64-bit). // Use [2]u64 (16 bytes) for future expansion. [2]u64, ); pub const C = c_union.C; pub const CValue = c_union.CValue; pub const cval = c_union.cval;};
/// Scroll the viewport of the terminal grid.pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) void { self.screens.active.scroll(switch (behavior) { .top => .{ .top = {} }, .bottom => .{ .active = {} }, .delta => |delta| .{ .delta_row = delta }, });}
/// To be called before shifting a row (as in insertLines and deleteLines)////// Takes care of boundary conditions such as potentially split wide chars/// across scrolling region boundaries and orphaned spacer heads at line/// ends.fn rowWillBeShifted( self: *Terminal, page: *Page, row: *Row,) void { const cells = row.cells.ptr(page.memory.ptr);
// If our scrolling region includes the rightmost column then we // need to turn any spacer heads in to normal empty cells, since // once we move them they no longer correspond with soft-wrapped // wide characters. // // If it contains either of the 2 leftmost columns, then the wide // characters in the first column which may be associated with a // spacer head will be either moved or cleared, so we also need // to turn the spacer heads in to empty cells in that case. if (self.scrolling_region.right == self.cols - 1 or self.scrolling_region.left < 2) { const end_cell: *Cell = &cells[page.size.cols - 1]; if (end_cell.wide == .spacer_head) { end_cell.wide = .narrow; } }
// If the leftmost or rightmost cells of our scrolling region // are parts of wide chars, we need to clear the cells' contents // since they'd be split by the move. const left_cell: *Cell = &cells[self.scrolling_region.left]; const right_cell: *Cell = &cells[self.scrolling_region.right];
if (left_cell.wide == .spacer_tail) { const wide_cell: *Cell = &cells[self.scrolling_region.left - 1]; if (wide_cell.hasGrapheme()) { page.clearGrapheme(wide_cell); page.updateRowGraphemeFlag(row); } wide_cell.content.codepoint = 0; wide_cell.wide = .narrow; left_cell.wide = .narrow; }
if (right_cell.wide == .wide) { const tail_cell: *Cell = &cells[self.scrolling_region.right + 1]; if (right_cell.hasGrapheme()) { page.clearGrapheme(right_cell); page.updateRowGraphemeFlag(row); } right_cell.content.codepoint = 0; right_cell.wide = .narrow; tail_cell.wide = .narrow; }}
// TODO(qwerasd): `insertLines` and `deleteLines` are 99% identical,// the majority of their logic can (and should) be abstracted in to// a single shared helper function, probably on `Screen` not here.// I'm just too lazy to do that rn :p
/// Insert amount lines at the current cursor row. The contents of the line/// at the current cursor row and below (to the bottom-most line in the/// scrolling region) are shifted down by amount lines. The contents of the/// amount bottom-most lines in the scroll region are lost.////// This unsets the pending wrap state without wrapping. If the current cursor/// position is outside of the current scroll region it does nothing.////// If amount is greater than the remaining number of lines in the scrolling/// region it is adjusted down (still allowing for scrolling out every remaining/// line in the scrolling region)////// In left and right margin mode the margins are respected; lines are only/// scrolled in the scroll region.////// All cleared space is colored according to the current SGR state.////// Moves the cursor to the left margin.pub fn insertLines(self: *Terminal, count: usize) void { // Rare, but happens if (count == 0) return;
// If the cursor is outside the scroll region we do nothing. if (self.screens.active.cursor.y < self.scrolling_region.top or self.screens.active.cursor.y > self.scrolling_region.bottom or self.screens.active.cursor.x < self.scrolling_region.left or self.screens.active.cursor.x > self.scrolling_region.right) return;
if (comptime build_options.kitty_graphics) { // Scrolling dirties the images because it updates their placements pins. self.screens.active.kitty_images.dirty = true; }
// At the end we need to return the cursor to the row it started on. const start_y = self.screens.active.cursor.y; defer { self.screens.active.cursorAbsolute(self.scrolling_region.left, start_y);
// Always unset pending wrap self.screens.active.cursor.pending_wrap = false; }
// We have a slower path if we have left or right scroll margins. const left_right = self.scrolling_region.left > 0 or self.scrolling_region.right < self.cols - 1;
// Remaining rows from our cursor to the bottom of the scroll region. const rem = self.scrolling_region.bottom - self.screens.active.cursor.y + 1;
// We can only insert lines up to our remaining lines in the scroll // region. So we take whichever is smaller. const adjusted_count = @min(count, rem);
// Create a new tracked pin which we'll use to navigate the page list // so that if we need to adjust capacity it will be properly tracked. var cur_p = self.screens.active.pages.trackPin( self.screens.active.cursor.page_pin.down(rem - 1).?, ) catch |err| { comptime assert(@TypeOf(err) == error{OutOfMemory});
// This error scenario means that our GPA is OOM. This is not a // situation we can gracefully handle. We can't just ignore insertLines // because it'll result in a corrupted screen. Ideally in the future // we flag the state as broken and show an error message to the user. // For now, we panic. log.err("insertLines trackPin error err={}", .{err}); @panic("insertLines trackPin OOM"); }; defer self.screens.active.pages.untrackPin(cur_p);
// Our current y position relative to the cursor var y: usize = rem;
// Traverse from the bottom up while (y > 0) { const cur_rac = cur_p.rowAndCell(); const cur_row: *Row = cur_rac.row;
// If this is one of the lines we need to shift, do so if (y > adjusted_count) { const off_p = cur_p.up(adjusted_count).?; const off_rac = off_p.rowAndCell(); const off_row: *Row = off_rac.row;
self.rowWillBeShifted(&cur_p.node.data, cur_row); self.rowWillBeShifted(&off_p.node.data, off_row);
// If our scrolling region is full width, then we unset wrap. if (!left_right) { off_row.wrap = false; cur_row.wrap = false; off_row.wrap_continuation = false; cur_row.wrap_continuation = false; }
const src_p = off_p; const src_row = off_row; const dst_p = cur_p; const dst_row = cur_row;
// If our page doesn't match, then we need to do a copy from // one page to another. This is the slow path. if (src_p.node != dst_p.node) { dst_p.node.data.clonePartialRowFrom( &src_p.node.data, dst_row, src_row, self.scrolling_region.left, self.scrolling_region.right + 1, ) catch |err| { // Adjust our page capacity to make // room for we didn't have space for _ = self.screens.active.increaseCapacity( dst_p.node, switch (err) { // Rehash the sets error.StyleSetNeedsRehash, error.HyperlinkSetNeedsRehash, => null,
// Increase style memory error.StyleSetOutOfMemory, => .styles,
// Increase string memory error.StringAllocOutOfMemory, => .string_bytes,
// Increase hyperlink memory error.HyperlinkSetOutOfMemory, error.HyperlinkMapOutOfMemory, => .hyperlink_bytes,
// Increase grapheme memory error.GraphemeMapOutOfMemory, error.GraphemeAllocOutOfMemory, => .grapheme_bytes, }, ) catch |e| switch (e) { // System OOM. We have no way to recover from this // currently. We should probably change insertLines // to raise an error here. error.OutOfMemory, => @panic("increaseCapacity system allocator OOM"),
// The page can't accommodate the managed memory required // for this operation. We previously just corrupted // memory here so a crash is better. The right long // term solution is to allocate a new page here // move this row to the new page, and start over. error.OutOfSpace, => @panic("increaseCapacity OutOfSpace"), };
// Continue the loop to try handling this row again. continue; }; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the // proper shifted rows and src gets non-garbage cell data that // we can clear. const dst = dst_row.*; dst_row.* = src_row.*; src_row.* = dst;
// Ensure what we did didn't corrupt the page cur_p.node.data.assertIntegrity(); } else { // Left/right scroll margins we have to // copy cells, which is much slower... const page = &cur_p.node.data; page.moveCells( src_row, self.scrolling_region.left, dst_row, self.scrolling_region.left, (self.scrolling_region.right - self.scrolling_region.left) + 1, ); } } } else { // Clear the cells for this row, it has been shifted. self.rowWillBeShifted(&cur_p.node.data, cur_row); const page = &cur_p.node.data; const cells = page.getCells(cur_row); self.screens.active.clearCells( page, cur_row, cells[self.scrolling_region.left .. self.scrolling_region.right + 1], ); }
// Mark the row as dirty cur_p.markDirty();
// We have successfully processed a line. y -= 1; // Move our pin up to the next row. if (cur_p.up(1)) |p| cur_p.* = p; }}
/// Removes amount lines from the current cursor row down. The remaining lines/// to the bottom margin are shifted up and space from the bottom margin up is/// filled with empty lines.////// If the current cursor position is outside of the current scroll region it/// does nothing. If amount is greater than the remaining number of lines in the/// scrolling region it is adjusted down.////// In left and right margin mode the margins are respected; lines are only/// scrolled in the scroll region.////// If the cell movement splits a multi cell character that character cleared,/// by replacing it by spaces, keeping its current attributes. All other/// cleared space is colored according to the current SGR state.////// Moves the cursor to the left margin.pub fn deleteLines(self: *Terminal, count: usize) void { // Rare, but happens if (count == 0) return;
// If the cursor is outside the scroll region we do nothing. if (self.screens.active.cursor.y < self.scrolling_region.top or self.screens.active.cursor.y > self.scrolling_region.bottom or self.screens.active.cursor.x < self.scrolling_region.left or self.screens.active.cursor.x > self.scrolling_region.right) return;
if (comptime build_options.kitty_graphics) { // Scrolling dirties the images because it updates their placements pins. self.screens.active.kitty_images.dirty = true; }
// At the end we need to return the cursor to the row it started on. const start_y = self.screens.active.cursor.y; defer { self.screens.active.cursorAbsolute(self.scrolling_region.left, start_y); // Always unset pending wrap self.screens.active.cursor.pending_wrap = false; }
// We have a slower path if we have left or right scroll margins. const left_right = self.scrolling_region.left > 0 or self.scrolling_region.right < self.cols - 1;
// Remaining rows from our cursor to the bottom of the scroll region. const rem = self.scrolling_region.bottom - self.screens.active.cursor.y + 1;
// We can only insert lines up to our remaining lines in the scroll // region. So we take whichever is smaller. const adjusted_count = @min(count, rem);
// Create a new tracked pin which we'll use to navigate the page list // so that if we need to adjust capacity it will be properly tracked. var cur_p = self.screens.active.pages.trackPin( self.screens.active.cursor.page_pin.*, ) catch |err| { // See insertLines comptime assert(@TypeOf(err) == error{OutOfMemory}); log.err("deleteLines trackPin error err={}", .{err}); @panic("deleteLines trackPin OOM"); }; defer self.screens.active.pages.untrackPin(cur_p);
// Our current y position relative to the cursor var y: usize = 0;
// Traverse from the top down while (y < rem) { const cur_rac = cur_p.rowAndCell(); const cur_row: *Row = cur_rac.row;
// If this is one of the lines we need to shift, do so if (y < rem - adjusted_count) { const off_p = cur_p.down(adjusted_count).?; const off_rac = off_p.rowAndCell(); const off_row: *Row = off_rac.row;
self.rowWillBeShifted(&cur_p.node.data, cur_row); self.rowWillBeShifted(&off_p.node.data, off_row);
// If our scrolling region is full width, then we unset wrap. if (!left_right) { off_row.wrap = false; cur_row.wrap = false; off_row.wrap_continuation = false; cur_row.wrap_continuation = false; }
const src_p = off_p; const src_row = off_row; const dst_p = cur_p; const dst_row = cur_row;
// If our page doesn't match, then we need to do a copy from // one page to another. This is the slow path. if (src_p.node != dst_p.node) { dst_p.node.data.clonePartialRowFrom( &src_p.node.data, dst_row, src_row, self.scrolling_region.left, self.scrolling_region.right + 1, ) catch |err| { // Adjust our page capacity to make // room for we didn't have space for _ = self.screens.active.increaseCapacity( dst_p.node, switch (err) { // Rehash the sets error.StyleSetNeedsRehash, error.HyperlinkSetNeedsRehash, => null,
// Increase style memory error.StyleSetOutOfMemory, => .styles,
// Increase string memory error.StringAllocOutOfMemory, => .string_bytes,
// Increase hyperlink memory error.HyperlinkSetOutOfMemory, error.HyperlinkMapOutOfMemory, => .hyperlink_bytes,
// Increase grapheme memory error.GraphemeMapOutOfMemory, error.GraphemeAllocOutOfMemory, => .grapheme_bytes, }, ) catch |e| switch (e) { // See insertLines error.OutOfMemory, => @panic("increaseCapacity system allocator OOM"),
error.OutOfSpace, => @panic("increaseCapacity OutOfSpace"), };
// Continue the loop to try handling this row again. continue; }; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the // proper shifted rows and src gets non-garbage cell data that // we can clear. const dst = dst_row.*; dst_row.* = src_row.*; src_row.* = dst;
// Ensure what we did didn't corrupt the page cur_p.node.data.assertIntegrity(); } else { // Left/right scroll margins we have to // copy cells, which is much slower... const page = &cur_p.node.data; page.moveCells( src_row, self.scrolling_region.left, dst_row, self.scrolling_region.left, (self.scrolling_region.right - self.scrolling_region.left) + 1, ); } } } else { // Clear the cells for this row, it's from out of bounds. self.rowWillBeShifted(&cur_p.node.data, cur_row); const page = &cur_p.node.data; const cells = page.getCells(cur_row); self.screens.active.clearCells( page, cur_row, cells[self.scrolling_region.left .. self.scrolling_region.right + 1], ); }
// Mark the row as dirty cur_p.markDirty();
// We have successfully processed a line. y += 1; // Move our pin down to the next row. if (cur_p.down(1)) |p| cur_p.* = p; }}
/// Inserts spaces at current cursor position moving existing cell contents/// to the right. The contents of the count right-most columns in the scroll/// region are lost. The cursor position is not changed.////// This unsets the pending wrap state without wrapping.////// The inserted cells are colored according to the current SGR state.pub fn insertBlanks(self: *Terminal, count: usize) void { // Unset pending wrap state without wrapping. Note: this purposely // happens BEFORE the scroll region check below, because that's what // xterm does. self.screens.active.cursor.pending_wrap = false;
// If we're given a zero then we do nothing. The rest of this function // assumes count > 0 and will crash if zero so return early. Note that // this shouldn't be possible with real CSI sequences because the value // is clamped to 1 min. if (count == 0) return;
// If our cursor is outside the margins then do nothing. We DO reset // wrap state still so this must remain below the above logic. if (self.screens.active.cursor.x < self.scrolling_region.left or self.screens.active.cursor.x > self.scrolling_region.right) return;
// If our count is larger than the remaining amount, we just erase right. // We only do this if we can erase the entire line (no right margin). // if (right_limit == self.cols and // count > right_limit - self.screens.active.cursor.x) // { // self.eraseLine(.right, false); // return; // }
// left is just the cursor position but as a multi-pointer const left: [*]Cell = @ptrCast(self.screens.active.cursor.page_cell); var page = &self.screens.active.cursor.page_pin.node.data;
// If our X is a wide spacer tail then we need to erase the // previous cell too so we don't split a multi-cell character. if (self.screens.active.cursor.page_cell.wide == .spacer_tail) { assert(self.screens.active.cursor.x > 0); self.screens.active.clearCells(page, self.screens.active.cursor.page_row, (left - 1)[0..2]); }
// Remaining cols from our cursor to the right margin. const rem = self.scrolling_region.right - self.screens.active.cursor.x + 1;
// If the cell at the right margin is wide, its spacer tail is // outside the scroll region and would be orphaned by either the // shift or the clear. Clean up both halves up front. { const right_cell: *Cell = @ptrCast(left + (rem - 1)); if (right_cell.wide == .wide) self.screens.active.clearCells( page, self.screens.active.cursor.page_row, @as([*]Cell, @ptrCast(right_cell))[0..2], ); }
// We can only insert blanks up to our remaining cols const adjusted_count = @min(count, rem);
// This is the amount of space at the right of the scroll region // that will NOT be blank, so we need to shift the correct cols right. // "scroll_amount" is the number of such cols. const scroll_amount = rem - adjusted_count; if (scroll_amount > 0) { page.pauseIntegrityChecks(true); defer page.pauseIntegrityChecks(false);
var x: [*]Cell = left + (scroll_amount - 1);
// If our last cell we're shifting is wide, then we need to clear // it to be empty so we don't split the multi-cell char. const end: *Cell = @ptrCast(x); if (end.wide == .wide) { const end_multi: [*]Cell = @ptrCast(end); assert(end_multi[1].wide == .spacer_tail); self.screens.active.clearCells( page, self.screens.active.cursor.page_row, end_multi[0..2], ); }
// We work backwards so we don't overwrite data. while (@intFromPtr(x) >= @intFromPtr(left)) : (x -= 1) { const src: *Cell = @ptrCast(x); const dst: *Cell = @ptrCast(x + adjusted_count); page.swapCells(src, dst); } }
// Insert blanks. The blanks preserve the background color. self.screens.active.clearCells(page, self.screens.active.cursor.page_row, left[0..adjusted_count]);
// Our row is always dirty self.screens.active.cursorMarkDirty();}
/// Removes amount characters from the current cursor position to the right./// The remaining characters are shifted to the left and space from the right/// margin is filled with spaces.////// If amount is greater than the remaining number of characters in the/// scrolling region, it is adjusted down.////// Does not change the cursor position.pub fn deleteChars(self: *Terminal, count_req: usize) void { if (count_req == 0) return;
// If our cursor is outside the margins then do nothing. We DO reset // wrap state still so this must remain below the above logic. if (self.screens.active.cursor.x < self.scrolling_region.left or self.screens.active.cursor.x > self.scrolling_region.right) return;
// left is just the cursor position but as a multi-pointer const left: [*]Cell = @ptrCast(self.screens.active.cursor.page_cell); var page = &self.screens.active.cursor.page_pin.node.data;
// Remaining cols from our cursor to the right margin. const rem = self.scrolling_region.right - self.screens.active.cursor.x + 1;
// We can only insert blanks up to our remaining cols const count = @min(count_req, rem);
self.screens.active.splitCellBoundary(self.screens.active.cursor.x); self.screens.active.splitCellBoundary(self.screens.active.cursor.x + count); self.screens.active.splitCellBoundary(self.scrolling_region.right + 1);
// This is the amount of space at the right of the scroll region // that will NOT be blank, so we need to shift the correct cols right. // "scroll_amount" is the number of such cols. const scroll_amount = rem - count; var x: [*]Cell = left; if (scroll_amount > 0) { page.pauseIntegrityChecks(true); defer page.pauseIntegrityChecks(false);
const right: [*]Cell = left + (scroll_amount - 1);
while (@intFromPtr(x) <= @intFromPtr(right)) : (x += 1) { const src: *Cell = @ptrCast(x + count); const dst: *Cell = @ptrCast(x); page.swapCells(src, dst); } }
// Insert blanks. The blanks preserve the background color. self.screens.active.clearCells(page, self.screens.active.cursor.page_row, x[0 .. rem - scroll_amount]);
// Our row's soft-wrap is always reset. self.screens.active.cursorResetWrap();
// Our row is always dirty self.screens.active.cursorMarkDirty();}
pub fn eraseChars(self: *Terminal, count_req: usize) void { const count = end: { const remaining = self.cols - self.screens.active.cursor.x; var end = @min(remaining, @max(count_req, 1));
// If our last cell is a wide char then we need to also clear the // cell beyond it since we can't just split a wide char. if (end != remaining) { const last = self.screens.active.cursorCellRight(end - 1); if (last.wide == .wide) end += 1; }
break :end end; };
// Handle any boundary conditions on the edges of the erased area. // // TODO(qwerasd): This isn't actually correct if you take in to account // protected modes. We need to figure out how to make `clearCells` or at // least `clearUnprotectedCells` handle boundary conditions... self.screens.active.splitCellBoundary(self.screens.active.cursor.x); self.screens.active.splitCellBoundary(self.screens.active.cursor.x + count);
// Reset our row's soft-wrap. self.screens.active.cursorResetWrap();
// Mark our cursor row as dirty self.screens.active.cursorMarkDirty();
// Clear the cells const cells: [*]Cell = @ptrCast(self.screens.active.cursor.page_cell);
// If we never had a protection mode, then we can assume no cells // are protected and go with the fast path. If the last protection // mode was not ISO we also always ignore protection attributes. if (self.screens.active.protected_mode != .iso) { self.screens.active.clearCells( &self.screens.active.cursor.page_pin.node.data, self.screens.active.cursor.page_row, cells[0..count], ); return; }
self.screens.active.clearUnprotectedCells( &self.screens.active.cursor.page_pin.node.data, self.screens.active.cursor.page_row, cells[0..count], );}
/// Erase the line.pub fn eraseLine( self: *Terminal, mode: csi.EraseLine, protected_req: bool,) void { // Get our start/end positions depending on mode. const start, const end = switch (mode) { .right => right: { var x = self.screens.active.cursor.x;
// If our X is a wide spacer tail then we need to erase the // previous cell too so we don't split a multi-cell character. if (x > 0 and self.screens.active.cursor.page_cell.wide == .spacer_tail) { x -= 1; }
// Reset our row's soft-wrap. self.screens.active.cursorResetWrap();
break :right .{ x, self.cols }; },
.left => left: { var x = self.screens.active.cursor.x;
// If our x is a wide char we need to delete the tail too. if (self.screens.active.cursor.page_cell.wide == .wide) { x += 1; }
break :left .{ 0, x + 1 }; },
// Note that it seems like complete should reset the soft-wrap // state of the line but in xterm it does not. .complete => .{ 0, self.cols },
else => { log.err("unimplemented erase line mode: {}", .{mode}); return; }, };
// All modes will clear the pending wrap state and we know we have // a valid mode at this point. self.screens.active.cursor.pending_wrap = false;
// We always mark our row as dirty self.screens.active.cursorMarkDirty();
// Start of our cells const cells: [*]Cell = cells: { const cells: [*]Cell = @ptrCast(self.screens.active.cursor.page_cell); break :cells cells - self.screens.active.cursor.x; };
// We respect protected attributes if explicitly requested (probably // a DECSEL sequence) or if our last protected mode was ISO even if its // not currently set. const protected = self.screens.active.protected_mode == .iso or protected_req;
// If we're not respecting protected attributes, we can use a fast-path // to fill the entire line. if (!protected) { self.screens.active.clearCells( &self.screens.active.cursor.page_pin.node.data, self.screens.active.cursor.page_row, cells[start..end], ); return; }
self.screens.active.clearUnprotectedCells( &self.screens.active.cursor.page_pin.node.data, self.screens.active.cursor.page_row, cells[start..end], );}
/// Erase the display.pub fn eraseDisplay( self: *Terminal, mode: csi.EraseDisplay, protected_req: bool,) void { // We respect protected attributes if explicitly requested (probably // a DECSEL sequence) or if our last protected mode was ISO even if its // not currently set. const protected = self.screens.active.protected_mode == .iso or protected_req;
switch (mode) { .scroll_complete => { self.screens.active.scrollClear() catch |err| { log.warn("scroll clear failed, doing a normal clear err={}", .{err}); self.eraseDisplay(.complete, protected_req); return; };
// Unsets pending wrap state self.screens.active.cursor.pending_wrap = false;
if (comptime build_options.kitty_graphics) { // Clear all Kitty graphics state for this screen self.screens.active.kitty_images.delete( self.screens.active.alloc, self, .{ .all = true }, ); } },
.complete => { // If we're on the primary screen and our last non-empty row is // a prompt, then we do a scroll_complete instead. This is a // heuristic to get the generally desirable behavior that ^L // at a prompt scrolls the screen contents prior to clearing. // Most shells send `ESC [ H ESC [ 2 J` so we can't just check // our current cursor position. See #905 if (self.screens.active_key == .primary) at_prompt: { // Go from the bottom of the active up and see if we're // at a prompt. const active_br = self.screens.active.pages.getBottomRight( .active, ) orelse break :at_prompt; var it = active_br.rowIterator( .left_up, self.screens.active.pages.getTopLeft(.active), ); while (it.next()) |p| { const row = p.rowAndCell().row; switch (row.semantic_prompt) { // If we're at a prompt or input area, then we are at a prompt. .prompt, .prompt_continuation, => break,
// If we have command output, then we're most certainly not // at a prompt. .none => break :at_prompt, } } else break :at_prompt;
self.screens.active.scrollClear() catch { // If we fail, we just fall back to doing a normal clear // so we don't worry about the error. }; }
// All active area self.screens.active.clearRows( .{ .active = .{} }, null, protected, );
// Unsets pending wrap state self.screens.active.cursor.pending_wrap = false;
if (comptime build_options.kitty_graphics) { // Clear all Kitty graphics state for this screen self.screens.active.kitty_images.delete( self.screens.active.alloc, self, .{ .all = true }, ); }
// Cleared screen dirty bit self.flags.dirty.clear = true; },
.below => { // All lines to the right (including the cursor) self.eraseLine(.right, protected_req);
// All lines below if (self.screens.active.cursor.y + 1 < self.rows) { self.screens.active.clearRows( .{ .active = .{ .y = self.screens.active.cursor.y + 1 } }, null, protected, ); }
// Unsets pending wrap state. Should be done by eraseLine. assert(!self.screens.active.cursor.pending_wrap); },
.above => { // Erase to the left (including the cursor) self.eraseLine(.left, protected_req);
// All lines above if (self.screens.active.cursor.y > 0) { self.screens.active.clearRows( .{ .active = .{ .y = 0 } }, .{ .active = .{ .y = self.screens.active.cursor.y - 1 } }, protected, ); }
// Unsets pending wrap state assert(!self.screens.active.cursor.pending_wrap); },
.scrollback => self.screens.active.eraseHistory(null), }}
/// Resets all margins and fills the whole screen with the character 'E'////// Sets the cursor to the top left corner.pub fn decaln(self: *Terminal) !void { // Clear our stylistic attributes. This is the only thing that can // fail so we do it first so we can undo it. const old_style = self.screens.active.cursor.style; self.screens.active.cursor.style = .{ .bg_color = self.screens.active.cursor.style.bg_color, .fg_color = self.screens.active.cursor.style.fg_color, }; errdefer self.screens.active.cursor.style = old_style; try self.screens.active.manualStyleUpdate();
// Reset margins, also sets cursor to top-left self.scrolling_region = .{ .top = 0, .bottom = self.rows - 1, .left = 0, .right = self.cols - 1, };
// Origin mode is disabled self.modes.set(.origin, false);
// Move our cursor to the top-left self.setCursorPos(1, 1);
// Use clearRows instead of eraseDisplay because we must NOT respect // protected attributes here. self.screens.active.clearRows( .{ .active = .{} }, null, false, );
// Fill with Es by moving the cursor but reset it after. while (true) { const page = &self.screens.active.cursor.page_pin.node.data; const row = self.screens.active.cursor.page_row; const cells_multi: [*]Cell = row.cells.ptr(page.memory); const cells = cells_multi[0..page.size.cols]; @memset(cells, .{ .content_tag = .codepoint, .content = .{ .codepoint = 'E' }, .style_id = self.screens.active.cursor.style_id,
// DECALN does not respect protected state. Verified with xterm. .protected = false, });
// If we have a ref-counted style, increase if (self.screens.active.cursor.style_id != style.default_id) { page.styles.useMultiple( page.memory, self.screens.active.cursor.style_id, @intCast(cells.len), ); row.styled = true; }
// We messed with the page so assert its integrity here. page.assertIntegrity();
self.screens.active.cursorMarkDirty(); if (self.screens.active.cursor.y == self.rows - 1) break; self.screens.active.cursorDown(1); }
// Reset the cursor to the top-left self.setCursorPos(1, 1);}
/// Execute a kitty graphics command. The buf is used to populate with/// the response that should be sent as an APC sequence. The response will/// be a full, valid APC sequence.////// If an error occurs, the caller should response to the pty that a/// an error occurred otherwise the behavior of the graphics protocol is/// undefined.pub fn kittyGraphics( self: *Terminal, alloc: Allocator, cmd: *kitty.graphics.Command,) ?kitty.graphics.Response { return kitty.graphics.execute(alloc, self, cmd);}
/// Execute a Glyph Protocol APC command against this terminal's per-session/// glossary. The returned response, if any, should be sent back to the pty as/// a complete APC sequence via `Response.formatWire`.pub fn glyphProtocol( self: *Terminal, alloc: Allocator, req: *const glyph.Request,) ?glyph.Response { const resp = glyph.execute(alloc, &self.glyph_glossary, req); switch (req.*) { .register, .clear => self.flags.dirty.glyph_glossary = true, .support, .query => {}, } return resp;}
/// Set the storage size limit for Kitty graphics across all screens.pub fn setKittyGraphicsSizeLimit( self: *Terminal, alloc: Allocator, limit: usize,) !void { if (comptime !build_options.kitty_graphics) return; var it = self.screens.all.iterator(); while (it.next()) |entry| { const screen: *Screen = entry.value.*; try screen.kitty_images.setLimit(alloc, screen, limit); }}
/// Set the allowed medium types for Kitty graphics image loading/// across all screens.pub fn setKittyGraphicsLoadingLimits( self: *Terminal, limits: kitty.graphics.LoadingImage.Limits,) void { if (comptime !build_options.kitty_graphics) return; var it = self.screens.all.iterator(); while (it.next()) |entry| { const screen: *Screen = entry.value.*; screen.kitty_images.image_limits = limits; }}
/// Set a style attribute.pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { try self.screens.active.setAttribute(attr);}
/// Print the active attributes as a string. This is used to respond to DECRQSS/// requests.////// Boolean attributes are printed first, followed by foreground color, then/// background color. Each attribute is separated by a semicolon.pub fn printAttributes(self: *Terminal, buf: []u8) ![]const u8 { var stream = std.io.fixedBufferStream(buf); const writer = stream.writer();
// The SGR response always starts with a 0. See https://vt100.net/docs/vt510-rm/DECRPSS try writer.writeByte('0');
const pen = self.screens.active.cursor.style; var attrs: [8]u8 = @splat(0); var i: usize = 0;
if (pen.flags.bold) { attrs[i] = '1'; i += 1; }
if (pen.flags.faint) { attrs[i] = '2'; i += 1; }
if (pen.flags.italic) { attrs[i] = '3'; i += 1; }
if (pen.flags.underline != .none) { attrs[i] = '4'; i += 1; }
if (pen.flags.blink) { attrs[i] = '5'; i += 1; }
if (pen.flags.inverse) { attrs[i] = '7'; i += 1; }
if (pen.flags.invisible) { attrs[i] = '8'; i += 1; }
if (pen.flags.strikethrough) { attrs[i] = '9'; i += 1; }
for (attrs[0..i]) |c| { try writer.print(";{c}", .{c}); }
switch (pen.fg_color) { .none => {}, .palette => |idx| if (idx >= 16) try writer.print(";38:5:{}", .{idx}) else if (idx >= 8) try writer.print(";9{}", .{idx - 8}) else try writer.print(";3{}", .{idx}), .rgb => |rgb| try writer.print(";38:2::{[r]}:{[g]}:{[b]}", rgb), }
switch (pen.bg_color) { .none => {}, .palette => |idx| if (idx >= 16) try writer.print(";48:5:{}", .{idx}) else if (idx >= 8) try writer.print(";10{}", .{idx - 8}) else try writer.print(";4{}", .{idx}), .rgb => |rgb| try writer.print(";48:2::{[r]}:{[g]}:{[b]}", rgb), }
return stream.getWritten();}
/// The modes for DECCOLM.pub const DeccolmMode = enum(u1) { @"80_cols" = 0, @"132_cols" = 1,};
/// DECCOLM changes the terminal width between 80 and 132 columns. This/// function call will do NOTHING unless `setDeccolmSupported` has been/// called with "true".////// This breaks the expectation around modern terminals that they resize/// with the window. This will fix the grid at either 80 or 132 columns./// The rows will continue to be variable.pub fn deccolm(self: *Terminal, alloc: Allocator, mode: DeccolmMode) !void { // If DEC mode 40 isn't enabled, then this is ignored. We also make // sure that we don't have deccolm set because we want to fully ignore // set mode. if (!self.modes.get(.enable_mode_3)) { self.modes.set(.@"132_column", false); return; }
// Enable it self.modes.set(.@"132_column", mode == .@"132_cols");
// Resize to the requested size try self.resize( alloc, switch (mode) { .@"132_cols" => 132, .@"80_cols" => 80, }, self.rows, );
// Erase our display and move our cursor. self.eraseDisplay(.complete, false); self.setCursorPos(1, 1);}
/// Resize the underlying terminal.pub fn resize( self: *Terminal, alloc: Allocator, cols: size.CellCountInt, rows: size.CellCountInt,) !void { // If our cols/rows didn't change then we're done if (self.cols == cols and self.rows == rows) return;
// Resize our tabstops if (self.cols != cols) { self.tabstops.deinit(alloc); self.tabstops = try .init(alloc, cols, 8); }
// Resize primary screen, which supports reflow const primary = self.screens.get(.primary).?; try primary.resize(.{ .cols = cols, .rows = rows, .reflow = self.modes.get(.wraparound), .prompt_redraw = self.flags.shell_redraws_prompt, });
// Alternate screen, if it exists, doesn't reflow if (self.screens.get(.alternate)) |alt| try alt.resize(.{ .cols = cols, .rows = rows, .reflow = false, });
// Whenever we resize we just mark it as a screen clear self.flags.dirty.clear = true;
// Set our size self.cols = cols; self.rows = rows;
// Reset the scrolling region self.scrolling_region = .{ .top = 0, .bottom = rows - 1, .left = 0, .right = cols - 1, };}
/// Set the pwd for the terminal.pub fn setPwd(self: *Terminal, pwd: []const u8) !void { self.pwd.clearRetainingCapacity(); if (pwd.len > 0) { try self.pwd.appendSlice(self.gpa(), pwd); try self.pwd.append(self.gpa(), 0); }}
/// Returns the pwd for the terminal, if any. The memory is owned by the/// Terminal and is not copied. It is safe until a reset or setPwd.pub fn getPwd(self: *const Terminal) ?[:0]const u8 { if (self.pwd.items.len == 0) return null; return self.pwd.items[0 .. self.pwd.items.len - 1 :0];}
/// Set the title for the terminal, as set by escape sequences (e.g. OSC 0/2).pub fn setTitle(self: *Terminal, t: []const u8) !void { self.title.clearRetainingCapacity(); if (t.len > 0) { try self.title.appendSlice(self.gpa(), t); try self.title.append(self.gpa(), 0); }}
/// Returns the title for the terminal, if any. The memory is owned by the/// Terminal and is not copied. It is safe until a reset or setTitle.pub fn getTitle(self: *const Terminal) ?[:0]const u8 { if (self.title.items.len == 0) return null; return self.title.items[0 .. self.title.items.len - 1 :0];}
/// Switch to the given screen type (alternate or primary).////// This does NOT handle behaviors such as clearing the screen,/// copying the cursor, etc. This should be handled by downstream/// callers.////// After calling this function, the `self.screen` field will point/// to the current screen, and the returned value will be the previous/// screen. If the return value is null, then the screen was not/// switched because it was already the active screen.////// Note: This is written in a generic way so that we can support/// more than two screens in the future if needed. There isn't/// currently a spec for this, but it is something I think might/// be useful in the future.pub fn switchScreen(self: *Terminal, key: ScreenSet.Key) !?*Screen { // If we're already on the requested screen we do nothing. if (self.screens.active_key == key) return null; const old = self.screens.active;
// We always end hyperlink state when switching screens. // We need to do this on the original screen. old.endHyperlink();
// Switch the screens/ const new = self.screens.get(key) orelse new: { const primary = self.screens.get(.primary).?; break :new try self.screens.getInit( old.alloc, key, .{ .cols = self.cols, .rows = self.rows, .max_scrollback = switch (key) { .primary => primary.pages.explicit_max_size, .alternate => 0, },
// Inherit our Kitty image settings from the primary // screen if we have to initialize. .kitty_image_storage_limit = if (comptime build_options.kitty_graphics) primary.kitty_images.total_limit else 0, .kitty_image_loading_limits = if (comptime build_options.kitty_graphics) primary.kitty_images.image_limits else {}, }, ); };
// The new screen should not have any hyperlinks set assert(new.cursor.hyperlink_id == 0);
// Bring our charset state with us new.charset = old.charset;
// Clear our selection new.clearSelection();
if (comptime build_options.kitty_graphics) { // Mark kitty images as dirty so they redraw. Without this set // the images will remain where they were (the dirty bit on // the screen only tracks the terminal grid, not the images). new.kitty_images.dirty = true; }
// Mark our terminal as dirty to redraw the grid. self.flags.dirty.clear = true;
// Finalize the switch self.screens.switchTo(key);
return old;}
/// Switch screen via a mode switch (e.g. mode 47, 1047, 1049)./// This is a much more opinionated operation than `switchScreen`/// since it also handles the behaviors of the specific mode,/// such as clearing the screen, saving/restoring the cursor,/// etc.////// This should be used for legacy compatibility with VT protocols,/// but more modern usage should use `switchScreen` instead and handle/// details like clearing the screen, cursor saving, etc. manually.pub fn switchScreenMode( self: *Terminal, mode: SwitchScreenMode, enabled: bool,) !void { // The behavior in this function is completely based on reading // the xterm source, specifically "charproc.c" for // `srm_ALTBUF`, `srm_OPT_ALTBUF`, and `srm_OPT_ALTBUF_CURSOR`. // We shouldn't touch anything in here without adding a unit // test AND verifying the behavior with xterm.
switch (mode) { .@"47" => {},
// If we're disabling 1047 and we're on alt screen then // we clear the screen. .@"1047" => if (!enabled and self.screens.active_key == .alternate) { self.eraseDisplay(.complete, false); },
// 1049 unconditionally saves the cursor on enabling, even // if we're already on the alternate screen. .@"1049" => if (enabled) self.saveCursor(), }
// Switch screens first to whatever we're going to. const to: ScreenSet.Key = if (enabled) .alternate else .primary; const old_ = try self.switchScreen(to);
switch (mode) { // For these modes, we need to copy the cursor. We only copy // the cursor if the screen actually changed, otherwise the // cursor is already copied. The cursor is copied regardless // of destination screen. .@"47", .@"1047" => if (old_) |old| { self.screens.active.cursorCopy(old.cursor, .{ .hyperlink = false, }) catch |err| { log.warn( "cursor copy failed entering alt screen err={}", .{err}, ); }; },
// Mode 1049 restores cursor on the primary screen when // we disable it. .@"1049" => if (enabled) { assert(self.screens.active_key == .alternate); self.eraseDisplay(.complete, false);
// When we enter alt screen with 1049, we always copy the // cursor from the primary screen (if we weren't already // on it). if (old_) |old| { self.screens.active.cursorCopy(old.cursor, .{ .hyperlink = false, }) catch |err| { log.warn( "cursor copy failed entering alt screen err={}", .{err}, ); }; } } else { assert(self.screens.active_key == .primary); self.restoreCursor(); }, }}
/// Modal screen changes. These map to the literal terminal/// modes to enable or disable alternate screen modes. They each/// have subtle behaviors so we define them as an enum here.pub const SwitchScreenMode = enum { /// Legacy alternate screen mode. This goes to the alternate /// screen or primary screen and only copies the cursor. The /// screen is not erased. @"47",
/// Alternate screen mode where the alternate screen is cleared /// on exit. The primary screen is never cleared. The cursor is /// copied. @"1047",
/// Save primary screen cursor, switch to alternate screen, /// and clear the alternate screen on entry. On exit, /// do not clear the screen, and restore the cursor on the /// primary screen. @"1049",};
/// Return the current string value of the terminal. Newlines are/// encoded as "\n". This omits any formatting such as fg/bg.////// The caller must free the string.pub fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 { return try self.screens.active.dumpStringAlloc(alloc, .{ .viewport = .{} });}
/// Same as plainString, but respects row wrap state when building the string.pub fn plainStringUnwrapped(self: *Terminal, alloc: Allocator) ![]const u8 { return try self.screens.active.dumpStringAllocUnwrapped(alloc, .{ .viewport = .{} });}
/// Full reset.////// This will attempt to free the existing screen memory but if that fails/// this will reuse the existing memory. In the latter case, memory may/// be wasted (since its unused) but it isn't leaked.pub fn fullReset(self: *Terminal) void { // Ensure we're back on primary screen self.screens.switchTo(.primary); self.screens.remove( self.screens.active.alloc, .alternate, );
// Reset our screens self.screens.active.reset();
// Rest our basic state self.modes.reset(); self.flags = .{}; self.tabstops.reset(TABSTOP_INTERVAL); self.previous_char = null; self.pwd.clearRetainingCapacity(); self.title.clearRetainingCapacity(); self.glyph_glossary.clearAndFree(self.gpa()); self.status_display = .main; self.scrolling_region = .{ .top = 0, .bottom = self.rows - 1, .left = 0, .right = self.cols - 1, };
// Always mark dirty so we redraw everything self.flags.dirty.clear = true;}
/// Returns true if the point is dirty, used for testing.fn isDirty(t: *const Terminal, pt: point.Point) bool { return t.screens.active.pages.getCell(pt).?.isDirty();}
/// Clear all dirty bits. Testing only.fn clearDirty(t: *Terminal) void { t.screens.active.pages.clearDirty();}
test "Terminal: input with no control characters" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 40, .rows = 40 }); defer t.deinit(alloc);
// Basic grid writing for ("hello") |c| try t.print(c); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x); { const str = try t.plainString(alloc); defer alloc.free(str); try testing.expectEqualStrings("hello", str); }
// The first row should be dirty try testing.expect(t.isDirty(.{ .screen = .{ .x = 5, .y = 0 } })); try testing.expect(!t.isDirty(.{ .screen = .{ .x = 5, .y = 1 } }));}
test "Terminal: input with basic wraparound" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 40 }); defer t.deinit(alloc);
// Basic grid writing for ("helloworldabc12") |c| try t.print(c); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.pending_wrap); { const str = try t.plainString(alloc); defer alloc.free(str); try testing.expectEqualStrings("hello\nworld\nabc12", str); }}
test "Terminal: input with basic wraparound dirty" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 40 }); defer t.deinit(alloc);
for ("hello") |c| try t.print(c); try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); t.clearDirty(); try t.print('w');
// Old row is dirty because cursor moved from there try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } }));}
test "Terminal: input that forces scroll" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 1, .rows = 5 }); defer t.deinit(alloc);
// Basic grid writing for ("abcdef") |c| try t.print(c); try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); { const str = try t.plainString(alloc); defer alloc.free(str); try testing.expectEqualStrings("b\nc\nd\ne\nf", str); }}
test "Terminal: input unique style per cell" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 30, .rows = 30 }); defer t.deinit(alloc);
for (0..t.rows) |y| { for (0..t.cols) |x| { t.setCursorPos(y, x); try t.setAttribute(.{ .direct_color_bg = .{ .r = @intCast(x), .g = @intCast(y), .b = 0, } }); try t.print('x'); } }}
test "Terminal: input glitch text" { const glitch = @embedFile("res/glitch.txt"); const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 30, .rows = 30 }); defer t.deinit(alloc);
// Get our initial grapheme capacity. const grapheme_cap = cap: { const page = t.screens.active.pages.pages.first.?; break :cap page.data.capacity.grapheme_bytes; };
// Print glitch text until our capacity changes while (true) { const page = t.screens.active.pages.pages.first.?; if (page.data.capacity.grapheme_bytes != grapheme_cap) break; try t.printString(glitch); }
// We're testing to make sure that grapheme capacity gets increased. const page = t.screens.active.pages.pages.first.?; try testing.expect(page.data.capacity.grapheme_bytes > grapheme_cap);}
test "Terminal: zero-width character at start" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// This used to crash the terminal. This is not allowed so we should // just ignore it. try t.print(0x200D);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
// Should not be dirty since we changed nothing. try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
// https://github.com/ghostty-org/ghostty/pull/12581test "Terminal: zero-width character attaches to pending wrap cell" { var t = try init(testing.allocator, .{ .cols = 2, .rows = 2 }); defer t.deinit(testing.allocator);
// Disable grapheme clustering to exercise the fallback path. t.modes.set(.grapheme_cluster, false);
try t.print('x'); try t.print('å'); try t.print(0x0332); // Combining low line.
const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("xå̲", str);}
// https://github.com/mitchellh/ghostty/issues/1400test "Terminal: print single very long line" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// This would crash for issue 1400. So the assertion here is // that we simply do not crash. for (0..1000) |_| try t.print('x');}
test "Terminal: print wide char" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print wide char at edge creates spacer head" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
t.setCursorPos(1, 10); try t.print(0x1F600); // Smiley face try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }
// Our first row just had a spacer head added which does not affect // rendering so only the place where the wide char was printed // should be marked. // BUT old row is dirty because cursor moved from there try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } }));}
test "Terminal: print wide char with 1-column width" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 1, .rows = 2 }); defer t.deinit(alloc);
try t.print('😀'); // 0x1F600
// This prints a space so we should be dirty. try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print wide char in single-width terminal" { var t = try init(testing.allocator, .{ .cols = 1, .rows = 80 }); defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.pending_wrap);
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print over wide char at 0,0" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face t.setCursorPos(0, 0); try t.print('A');
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } }));}
test "Terminal: print over wide char at col 0 corrupts previous row" { // Crash found by AFL++ fuzzer (afl-out/stream/default/crashes/id:000002). // // printCell, when overwriting a wide cell with a narrow cell at x<=1 // and y>0, sets the last cell of the previous row to .narrow — even // when that cell is a .spacer_tail rather than a .spacer_head. This // orphans the .wide cell at cols-2. const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 3 }); defer t.deinit(alloc);
// Fill rows 0 and 1 with wide chars (5 per row on a 10-col terminal). for (0..10) |_| try t.print(0x4E2D);
// Move cursor to row 1, col 0 (on top of a wide char) and print a // narrow character. This triggers printCell's .wide branch which // corrupts row 0's last cell: col 9 changes from .spacer_tail to // .narrow, orphaning the .wide at col 8. t.setCursorPos(2, 1); try t.print('A');
// Row 1, col 0 should be narrow (we just overwrote the wide char). { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; try testing.expectEqual(Cell.Wide.narrow, list_cell.cell.wide); } // Row 0, col 8 should still be .wide (the last wide char on the row). { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 8, .y = 0 } }).?; try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide); } // Row 0, col 9 must remain .spacer_tail to pair with the .wide at col 8. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?; try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide); }}
test "Terminal: print over wide spacer tail" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
try t.print('橋'); t.setCursorPos(1, 2); try t.print('X');
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'X'), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print over wide char with bold" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
try t.setAttribute(.{ .bold = {} }); try t.print(0x1F600); // Smiley face // verify we have styles in our style map { const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); }
// Go back and overwrite with no style t.setCursorPos(0, 0); try t.setAttribute(.{ .unset = {} }); try t.print('A'); // Smiley face
// verify our style is gone { const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count()); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print over wide char with bg color" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); try t.print(0x1F600); // Smiley face // verify we have styles in our style map { const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); }
// Go back and overwrite with no style t.setCursorPos(0, 0); try t.setAttribute(.{ .unset = {} }); try t.print('A'); // Smiley face
// verify our style is gone { const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count()); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print multicodepoint grapheme, disabled mode 2027" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// https://github.com/mitchellh/ghostty/issues/289 // This is: 👨👩👧 (which may or may not render correctly) try t.print(0x1F468); try t.print(0x200D); try t.print(0x1F469); try t.print(0x200D); try t.print(0x1F467);
// We should have 6 cells taken up try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 6), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); try testing.expect(list_cell.node.data.lookupGrapheme(cell) == null); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F469), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); try testing.expect(list_cell.node.data.lookupGrapheme(cell) == null); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F467), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); try testing.expect(list_cell.node.data.lookupGrapheme(cell) == null); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 5, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); try testing.expect(list_cell.node.data.lookupGrapheme(cell) == null); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: VS16 doesn't make character with 2027 disabled" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Disable grapheme clustering t.modes.set(.grapheme_cluster, false);
try t.print(0x2764); // Heart try t.print(0xFE0F); // VS16 to make wide
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("❤️", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); }}
test "Terminal: ignored VS16 doesn't mark dirty" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Disable grapheme clustering t.modes.set(.grapheme_cluster, false);
try t.print(0x2764); // Heart try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
t.clearDirty(); try t.print(0xFE0F); // VS16 to make wide try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print invalid VS16 non-grapheme" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// https://github.com/mitchellh/ghostty/issues/1482 try t.print('x'); try t.print(0xFE0F);
// We should have 1 narrow cell. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); }}
test "Terminal: invalid VS16 doesn't mark dirty" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Disable grapheme clustering t.modes.set(.grapheme_cluster, false);
try t.print('x'); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
t.clearDirty(); try t.print(0xFE0F); // VS16 to make wide try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
// https://github.com/ghostty-org/ghostty/pull/12596test "Terminal: variation selectors apply to preceding codepoint" { var t = try init(testing.allocator, .{ .cols = 5, .rows = 5 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// Pirate flag: black flag + ZWJ + skull and crossbones + VS16. try t.print(0x1F3F4); try t.print(0x200D); try t.print(0x2620); try t.print(0xFE0F);
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F3F4), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{ 0x200D, 0x2620, 0xFE0F }, list_cell.node.data.lookupGrapheme(cell).?);}
test "Terminal: print multicodepoint grapheme, mode 2027" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/289 // This is: 👨👩👧 (which may or may not render correctly) try t.print(0x1F468); try t.print(0x200D); try t.print(0x1F469); try t.print(0x200D); try t.print(0x1F467);
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Row should be dirty try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 4), cps.len); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: keypad sequence VS15" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// This is: "#︎" (number sign with text presentation selector) try t.print(0x23); // # Number sign (valid base) try t.print(0xFE0E); // VS15 (text presentation selector)
// VS15 should combine with the base character into a single grapheme cluster, // taking 1 cell (narrow character). try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
// Row should be dirty try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// The base emoji should be in cell 0 with the skin tone as a grapheme { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x23), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: keypad sequence VS16" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// This is: "#️" (number sign with emoji presentation selector) try t.print(0x23); // # Number sign (valid base) try t.print(0xFE0F); // VS16 (emoji presentation selector)
// VS16 should combine with the base character into a single grapheme cluster, // taking 2 cells (wide character). try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Row should be dirty try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// The base emoji should be in cell 0 with the skin tone as a grapheme { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x23), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); }}
test "Terminal: Fitzpatrick skin tone next valid base" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// This is: "👋🏿" (waving hand with dark skin tone) try t.print(0x1F44B); // 👋 Waving hand (valid base) try t.print(0x1F3FF); // 🏿 Dark skin tone modifier
// The skin tone should combine with the base emoji into a single grapheme cluster, // taking 2 cells (wide character). try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Row should be dirty try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// The base emoji should be in cell 0 with the skin tone as a grapheme { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F44B), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); }}
test "Terminal: Fitzpatrick skin tone next to non-base" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// This is: "🏿" (which may not render correctly in your editor!) try t.print(0x22); // " try t.print(0x1F3FF); // Dark skin tone try t.print(0x22); // "
// We should have 4 cells taken up. Importantly, the skin tone // should not join with the quotes. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.x);
// Row should be dirty try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x22), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F3FF), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x22), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: multicodepoint grapheme marks dirty on every codepoint" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/289 // This is: 👨👩👧 (which may or may not render correctly) try t.print(0x1F468); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty(); try t.print(0x200D); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty(); try t.print(0x1F469); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty(); try t.print(0x200D); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty(); try t.print(0x1F467); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);}
test "Terminal: VS15 to make narrow character" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.print(0x2614); // Umbrella with rain drops, width=2 try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty();
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
try t.print(0xFE0E); // VS15 to make narrow try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty();
// VS15 should send us back a cell since our char is no longer wide. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("☔︎", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); }}
test "Terminal: VS15 on already narrow emoji" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.print(0x26C8); // Thunder cloud and rain, width=1 try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty(); try t.print(0xFE0E); // VS15 to make narrow try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty();
// Character takes up one cell try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("⛈︎", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x26C8), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); }}
test "Terminal: print invalid VS15 following emoji is wide" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.print('\u{1F9E0}'); // 🧠 try t.print(0xFE0E); // not valid with U+1F9E0 as base
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, '\u{1F9E0}'), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: print invalid VS15 in emoji ZWJ sequence" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.print('\u{1F469}'); // 👩 try t.print(0xFE0E); // not valid with U+1F469 as base try t.print('\u{200D}'); // ZWJ try t.print('\u{1F466}'); // 👦
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, '\u{1F469}'), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{ '\u{200D}', '\u{1F466}' }, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: VS15 to make narrow character with pending wrap" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 4 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try testing.expect(t.modes.get(.wraparound));
try t.print(0x1F34B); // Lemon, width=2 try t.print(0x2614); // Umbrella with rain drops, width=2 try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty();
// We only move to the end of the line because we're in a pending wrap // state. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.pending_wrap);
try t.print(0xFE0E); // VS15 to make narrow try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty();
// VS15 should clear the pending wrap state try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x); try testing.expect(!t.screens.active.cursor.pending_wrap);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("🍋☔︎", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); }
// VS15 should not affect the previous grapheme { const lemon_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?.cell; try testing.expectEqual(@as(u21, 0x1F34B), lemon_cell.content.codepoint); try testing.expectEqual(Cell.Wide.wide, lemon_cell.wide); const spacer_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?.cell; try testing.expectEqual(@as(u21, 0), spacer_cell.content.codepoint); try testing.expectEqual(Cell.Wide.spacer_tail, spacer_cell.wide); }}
test "Terminal: VS16 to make wide character on next line" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
t.cursorRight(2); try t.print('#'); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.pending_wrap); try testing.expect(t.isDirty(.{ .screen = .{ .x = 2, .y = 0 } })); t.clearDirty();
try t.print(0xFE0F); // VS16 to make wide
try testing.expect(t.isDirty(.{ .screen = .{ .x = 2, .y = 0 } })); t.clearDirty(); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expect(!t.screens.active.cursor.pending_wrap);
{ // Previous cell turns into spacer_head const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); } { // '#' cell is wide const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, '#'), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { // spacer_tail const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: VS16 to make wide character on next line with hyperlink" { // Regression test for the crash fixed in print's grapheme `.wide` path: // writing a spacer_head at the screen edge before row.wrap was set. var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering and activate a hyperlink so printCell // calls cursorSetHyperlink (which runs page integrity checks). t.modes.set(.grapheme_cluster, true); try t.screens.active.startHyperlink("http://example.com", null);
t.cursorRight(2); try t.print('#'); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.pending_wrap);
// Without the fix, this panicked with UnwrappedSpacerHead. try t.print(0xFE0F); // VS16 to make wide
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expect(!t.screens.active.cursor.pending_wrap);
{ // Previous cell turns into spacer_head and remains hyperlinked. const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); try testing.expect(cell.hyperlink); try testing.expect(list_cell.row.wrap); } { // '#' cell is now wide and still hyperlinked. const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, '#'), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.wide, cell.wide); try testing.expect(cell.hyperlink); } { // spacer_tail inherits hyperlink as part of the same grapheme cell. const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); try testing.expect(cell.hyperlink); }}
test "Terminal: VS16 to make wide character with pending wrap" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
t.cursorRight(1); try t.print('#'); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expect(!t.screens.active.cursor.pending_wrap);
try t.print(0xFE0F); // VS16 to make wide
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expect(t.screens.active.cursor.pending_wrap);
{ // '#' cell is wide const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, '#'), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { // spacer_tail const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: VS16 to make wide character with mode 2027" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.print(0x2764); // Heart try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty(); try t.print(0xFE0F); // VS16 to make wide try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("❤️", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); }}
test "Terminal: VS16 repeated with mode 2027" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.print(0x2764); // Heart try t.print(0xFE0F); // VS16 to make wide try t.print(0x2764); // Heart try t.print(0xFE0F); // VS16 to make wide
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("❤️❤️", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.wide, cell.wide); const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); }}
test "Terminal: print invalid VS16 grapheme" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/1482 try t.print('x'); try t.print(0xFE0F); // invalid VS16
// We should have 1 cells taken up, and narrow. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: print invalid VS16 with second char" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/1482 try t.print('x'); try t.print(0xFE0F); try t.print('y');
// We should have 2 cells taken up, from two separate narrow characters. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'y'), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: print grapheme ò (o with nonspacing mark) should be narrow" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.print('o'); try t.print(0x0300); // combining grave accent
// We should have 1 cell taken up. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'o'), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{0x0300}, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: print Devanagari grapheme should be wide" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// क्ष try t.print(0x0915); try t.print(0x094D); try t.print(0x200D); try t.print(0x0937);
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: print Devanagari grapheme should be wide on next line" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
t.cursorRight(2);
// क्ष try t.print(0x0915); try t.print(0x094D); try t.print(0x200D); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.pending_wrap);
// This one increases the width to wide try t.print(0x0937);
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expect(!t.screens.active.cursor.pending_wrap);
{ // Previous cell turns into spacer_head const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); } { // Devanagari grapheme is wide const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: print Devanagari grapheme should be wide on next page" { const rows = pagepkg.std_capacity.rows; const cols = pagepkg.std_capacity.cols; var t = try init(testing.allocator, .{ .rows = rows, .cols = cols }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
t.cursorDown(rows - 1);
for (rows..t.screens.active.pages.pages.first.?.data.capacity.rows) |_| { try t.index(); }
t.cursorRight(cols - 1);
try testing.expectEqual(cols - 1, t.screens.active.cursor.x); try testing.expectEqual(rows - 1, t.screens.active.cursor.y);
// क्ष try t.print(0x0915); try t.print(0x094D); try t.print(0x200D); try testing.expectEqual(cols - 1, t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.pending_wrap);
// This one increases the width to wide try t.print(0x0937);
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(rows - 1, t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expect(!t.screens.active.cursor.pending_wrap);
{ // Previous cell turns into spacer_head const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = cols - 1, .y = rows - 2 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); } { // Devanagari grapheme is wide const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = rows - 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 1, .y = rows - 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: print invalid VS16 with second char (combining)" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/1482 try t.print('n'); try t.print(0xFE0F); // invalid VS16 try t.print(0x0303); // combining tilde
// We should have 1 cells taken up, and narrow. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'n'), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); try testing.expectEqualSlices(u21, &.{'\u{0303}'}, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: overwrite grapheme should clear grapheme data" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.print(0x26C8); // Thunder cloud and rain try t.print(0xFE0E); // VS15 to make narrow try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty();
t.setCursorPos(1, 1); try t.print('A'); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: overwrite multicodepoint grapheme clears grapheme data" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/289 // This is: 👨👩👧 (which may or may not render correctly) try t.print(0x1F468); try t.print(0x200D); try t.print(0x1F469); try t.print(0x200D); try t.print(0x1F467);
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// We should have one cell with graphemes const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount());
// Move back and overwrite wide t.setCursorPos(1, 1); t.clearDirty(); try t.print('X'); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 0), page.graphemeCount());
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X", str); }}
test "Terminal: overwrite multicodepoint grapheme tail clears grapheme data" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/289 // This is: 👨👩👧 (which may or may not render correctly) try t.print(0x1F468); try t.print(0x200D); try t.print(0x1F469); try t.print(0x200D); try t.print(0x1F467);
// We should have 2 cells taken up. It is one character but "wide". try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// We should have one cell with graphemes const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount());
// Move back and overwrite wide t.setCursorPos(1, 2); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); }
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 0), page.graphemeCount());}
test "Terminal: print breaks valid grapheme cluster with Prepend + ASCII for speed" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc); t.modes.set(.grapheme_cluster, true);
// Make sure we're not at cursor.x == 0 for the next char. try t.print('_');
// U+0600 ARABIC NUMBER SIGN (Prepend) try t.print(0x0600); try t.print('1');
// We should have 3 cells taken up, each narrow. Note that this is // **incorrect** grapheme break behavior, since a Prepend code point should // not break with the one following it per UAX #29 GB9b. However, as an // optimization we assume a grapheme break when c <= 255, and note that // this deviation only affects these very uncommon scenarios (e.g. the // Arabic number sign should precede Arabic-script digits). try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x); // This is what we'd expect if we did break correctly: //try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Assert various properties about our screen to verify // we have all expected cells. { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x0600), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); // This is what we'd expect if we did break correctly: //try testing.expect(cell.hasGrapheme()); //try testing.expectEqualSlices(u21, &.{'1'}, list_cell.node.data.lookupGrapheme(cell).?); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, '1'), cell.content.codepoint); // This is what we'd expect if we did break correctly: //try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expect(!cell.hasGrapheme()); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: print writes to bottom if scrolled" { var t = try init(testing.allocator, .{ .cols = 5, .rows = 2 }); defer t.deinit(testing.allocator);
// Basic grid writing for ("hello") |c| try t.print(c); t.setCursorPos(0, 0);
// Make newlines so we create scrollback // 3 pushes hello off the screen try t.index(); try t.index(); try t.index(); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }
// Scroll to the top t.screens.active.scroll(.{ .top = {} }); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("hello", str); }
// Type try t.print('A'); t.screens.active.scroll(.{ .active = {} }); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nA", str); }
try testing.expect(t.isDirty(.{ .active = .{ .x = t.screens.active.cursor.x, .y = t.screens.active.cursor.y, } }));}
test "Terminal: print charset" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// G1 should have no effect t.configureCharset(.G1, .dec_special); t.configureCharset(.G2, .dec_special); t.configureCharset(.G3, .dec_special);
// No dirty to configure charset try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// Basic grid writing try t.print('`'); t.configureCharset(.G0, .utf8); try t.print('`'); t.configureCharset(.G0, .ascii); try t.print('`'); t.configureCharset(.G0, .dec_special); try t.print('`'); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("```◆", str); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print charset outside of ASCII" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// G1 should have no effect t.configureCharset(.G1, .dec_special); t.configureCharset(.G2, .dec_special); t.configureCharset(.G3, .dec_special);
// No dirty to configure charset try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// Basic grid writing t.configureCharset(.G0, .dec_special); try t.print('`'); try t.print(0x1F600); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("◆ ", str); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print invoke charset" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
t.configureCharset(.G1, .dec_special);
try t.print('`');
// Invokecharset but should not mark dirty on its own t.clearDirty(); t.invokeCharset(.GL, .G1, false); try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try t.print('`'); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try t.print('`'); t.invokeCharset(.GL, .G0, false); try t.print('`'); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("`◆◆`", str); }}
test "Terminal: print invoke charset single" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
t.configureCharset(.G1, .dec_special);
// Basic grid writing try t.print('`'); t.invokeCharset(.GL, .G1, true); try t.print('`'); try t.print('`'); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("`◆`", str); }}
test "Terminal: print kitty unicode placeholder" { if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
try t.print(kitty.graphics.unicode.placeholder); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, kitty.graphics.unicode.placeholder), cell.content.codepoint); try testing.expect(list_cell.row.kitty_virtual_placeholder); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: soft wrap" { var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 }); defer t.deinit(testing.allocator);
// Basic grid writing for ("hello") |c| try t.print(c); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("hel\nlo", str); }}
test "Terminal: soft wrap with semantic prompt" { var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 }); defer t.deinit(testing.allocator);
// Mark our prompt. try t.semanticPrompt(.init(.prompt_start)); // Should not make anything dirty on its own. try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
// Write and wrap for ("hello") |c| try t.print(c); { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; try testing.expectEqual(.prompt, list_cell.row.semantic_prompt); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt); }}
test "Terminal: disabled wraparound with wide char and one space" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
t.modes.set(.wraparound, false);
// This puts our cursor at the end and there is NO SPACE for a // wide character. try t.printString("AAAA"); t.clearDirty(); try t.print(0x1F6A8); // Police car light try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.x);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AAAA", str); }
// Make sure we printed nothing { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }
// Should not be dirty since we didn't modify anything try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: disabled wraparound with wide char and no space" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
t.modes.set(.wraparound, false);
// This puts our cursor at the end and there is NO SPACE for a // wide character. try t.printString("AAAAA"); t.clearDirty(); try t.print(0x1F6A8); // Police car light try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.x);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AAAAA", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }
// Should not be dirty since we didn't modify anything try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: disabled wraparound with wide grapheme and half space" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);
t.modes.set(.grapheme_cluster, true); t.modes.set(.wraparound, false);
// This puts our cursor at the end and there is NO SPACE for a // wide character. try t.printString("AAAA"); try t.print(0x2764); // Heart t.clearDirty(); try t.print(0xFE0F); // VS16 to make wide try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.x);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AAAA❤", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, '❤'), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }
// Should not be dirty since we didn't modify anything try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print right margin wrap" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); defer t.deinit(testing.allocator);
try t.printString("123456789"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setCursorPos(1, 5); try t.printString("XY");
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1234X6789\n Y", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; const row = list_cell.row; try testing.expect(!row.wrap); }}
test "Terminal: print right margin wrap dirty tracking" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); defer t.deinit(testing.allocator);
try t.printString("123456789"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setCursorPos(1, 5);
// Writing our X on the first line should mark only that line dirty. t.clearDirty(); try t.print('X'); try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); try testing.expect(!t.isDirty(.{ .screen = .{ .x = 2, .y = 1 } }));
// Writing our Y should wrap. It marks both rows dirty because the // cursor moved. t.clearDirty(); try t.print('Y'); try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); try testing.expect(t.isDirty(.{ .screen = .{ .x = 2, .y = 1 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1234X6789\n Y", str); }}
test "Terminal: print right margin outside" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); defer t.deinit(testing.allocator);
try t.printString("123456789"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setCursorPos(1, 6); t.clearDirty(); try t.printString("XY");
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("12345XY89", str); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 5, .y = 0 } }));}
test "Terminal: print right margin outside wrap" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); defer t.deinit(testing.allocator);
try t.printString("123456789"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setCursorPos(1, 10); try t.printString("XY");
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("123456789X\n Y", str); }}
test "Terminal: print wide char at right margin does not create spacer head" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setCursorPos(1, 5); try t.print(0x1F600); // Smiley face try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.x);
// Both rows dirty because the cursor moved try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 1 } }));
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide);
const row = list_cell.row; try testing.expect(!row.wrap); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: print with hyperlink" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Setup our hyperlink and print try t.screens.active.startHyperlink("http://example.com", null); try t.printString("123456");
// Verify all our cells have a hyperlink for (0..6) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print over cell with same hyperlink" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Setup our hyperlink and print try t.screens.active.startHyperlink("http://example.com", null); try t.printString("123456"); t.setCursorPos(1, 1); try t.printString("123456");
// Verify all our cells have a hyperlink for (0..6) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print and end hyperlink" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Setup our hyperlink and print try t.screens.active.startHyperlink("http://example.com", null); try t.printString("123"); t.screens.active.endHyperlink(); try t.printString("456");
// Verify all our cells have a hyperlink for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } for (3..6) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: print and change hyperlink" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Setup our hyperlink and print try t.screens.active.startHyperlink("http://one.example.com", null); try t.printString("123"); try t.screens.active.startHyperlink("http://two.example.com", null); try t.printString("456");
// Verify all our cells have a hyperlink for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } for (3..6) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 2), id); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
test "Terminal: overwrite hyperlink" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Setup our hyperlink and print try t.screens.active.startHyperlink("http://one.example.com", null); try t.printString("123"); t.setCursorPos(1, 1); t.screens.active.endHyperlink(); try t.printString("456");
// Verify all our cells have a hyperlink for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const page = &list_cell.node.data; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); try testing.expect(page.lookupHyperlink(cell) == null); try testing.expectEqual(0, page.hyperlink_set.count()); }
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));}
// Printing a wide char at the right edge with an active hyperlink causes// printCell to write a spacer_head before printWrap sets the row wrap// flag. The integrity check inside setHyperlink (or increaseCapacity)// sees the unwrapped spacer head and panics. Found via fuzzing.test "Terminal: print wide char at right edge with hyperlink" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); defer t.deinit(testing.allocator);
try t.screens.active.startHyperlink("http://example.com", null);
// Move cursor to the last column (1-indexed) t.setCursorPos(1, 10);
// Print a wide character; this will call printCell(0, .spacer_head) // at the right edge before calling printWrap, triggering the // integrity violation. try t.print(0x4E2D); // U+4E2D '中'
// Cursor wraps to row 2, after the wide char + spacer tail try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Row 0, col 9: spacer head with hyperlink { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?; try testing.expectEqual(Cell.Wide.spacer_head, list_cell.cell.wide); try testing.expect(list_cell.cell.hyperlink); try testing.expect(list_cell.row.wrap); } // Row 1, col 0: the wide char with hyperlink { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; try testing.expectEqual(@as(u21, 0x4E2D), list_cell.cell.content.codepoint); try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide); try testing.expect(list_cell.cell.hyperlink); } // Row 1, col 1: spacer tail with hyperlink { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide); try testing.expect(list_cell.cell.hyperlink); }}
test "Terminal: linefeed and carriage return" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Print and CR. for ("hello") |c| try t.print(c); t.clearDirty(); t.carriageReturn();
// CR should not mark row dirty because it doesn't change rendering. try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
try t.linefeed();
// LF marks row dirty due to cursor movement try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } }));
for ("world") |c| try t.print(c); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("hello\nworld", str); }}
test "Terminal: linefeed unsets pending wrap" { var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 }); defer t.deinit(testing.allocator);
// Basic grid writing for ("hello") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap == true); t.clearDirty(); try t.linefeed(); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } })); try testing.expect(t.screens.active.cursor.pending_wrap == false);}
test "Terminal: linefeed mode automatic carriage return" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
// Basic grid writing t.modes.set(.linefeed, true); try t.printString("123456"); try t.linefeed(); try t.print('X'); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("123456\nX", str); }}
test "Terminal: carriage return unsets pending wrap" { var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 }); defer t.deinit(testing.allocator);
// Basic grid writing for ("hello") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap == true); t.carriageReturn(); try testing.expect(t.screens.active.cursor.pending_wrap == false);}
test "Terminal: carriage return origin mode moves to left margin" { var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 }); defer t.deinit(testing.allocator);
t.modes.set(.origin, true); t.screens.active.cursor.x = 0; t.scrolling_region.left = 2; t.carriageReturn(); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);}
test "Terminal: carriage return left of left margin moves to zero" { var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 }); defer t.deinit(testing.allocator);
t.screens.active.cursor.x = 1; t.scrolling_region.left = 2; t.carriageReturn(); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);}
test "Terminal: carriage return right of left margin moves to left margin" { var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 }); defer t.deinit(testing.allocator);
t.screens.active.cursor.x = 3; t.scrolling_region.left = 2; t.carriageReturn(); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);}
test "Terminal: backspace" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// BS for ("hello") |c| try t.print(c); t.backspace(); try t.print('y'); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("helly", str); }}
test "Terminal: horizontal tabs" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 20, .rows = 5 }); defer t.deinit(alloc);
// HT try t.print('1'); t.horizontalTab(); try testing.expectEqual(@as(usize, 8), t.screens.active.cursor.x);
// HT t.horizontalTab(); try testing.expectEqual(@as(usize, 16), t.screens.active.cursor.x);
// HT at the end t.horizontalTab(); try testing.expectEqual(@as(usize, 19), t.screens.active.cursor.x); t.horizontalTab(); try testing.expectEqual(@as(usize, 19), t.screens.active.cursor.x);}
test "Terminal: horizontal tabs starting on tabstop" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 20, .rows = 5 }); defer t.deinit(alloc);
t.setCursorPos(t.screens.active.cursor.y, 9); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y, 9); t.horizontalTab(); try t.print('A');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X A", str); }}
test "Terminal: horizontal tabs with right margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 20, .rows = 5 }); defer t.deinit(alloc);
t.scrolling_region.left = 2; t.scrolling_region.right = 5; t.setCursorPos(t.screens.active.cursor.y, 1); try t.print('X'); t.horizontalTab(); try t.print('A');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X A", str); }}
test "Terminal: horizontal tabs back" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 20, .rows = 5 }); defer t.deinit(alloc);
// Edge of screen t.setCursorPos(t.screens.active.cursor.y, 20);
// HT t.horizontalTabBack(); try testing.expectEqual(@as(usize, 16), t.screens.active.cursor.x);
// HT t.horizontalTabBack(); try testing.expectEqual(@as(usize, 8), t.screens.active.cursor.x);
// HT t.horizontalTabBack(); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); t.horizontalTabBack(); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);}
test "Terminal: horizontal tabs back starting on tabstop" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 20, .rows = 5 }); defer t.deinit(alloc);
t.setCursorPos(t.screens.active.cursor.y, 9); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y, 9); t.horizontalTabBack(); try t.print('A');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A X", str); }}
test "Terminal: horizontal tabs with left margin in origin mode" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 20, .rows = 5 }); defer t.deinit(alloc);
t.modes.set(.origin, true); t.scrolling_region.left = 2; t.scrolling_region.right = 5; t.setCursorPos(1, 2); try t.print('X'); t.horizontalTabBack(); try t.print('A');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" AX", str); }}
test "Terminal: horizontal tab back with cursor before left margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 20, .rows = 5 }); defer t.deinit(alloc);
t.modes.set(.origin, true); t.saveCursor(); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(5, 0); t.restoreCursor(); t.horizontalTabBack(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X", str); }}
test "Terminal: cursorPos resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.setCursorPos(1, 1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("XBCDE", str); }}
test "Terminal: cursorPos off the screen" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setCursorPos(500, 500); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n\n\n X", str); }}
test "Terminal: cursorPos relative to origin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.scrolling_region.top = 2; t.scrolling_region.bottom = 3; t.modes.set(.origin, true); t.setCursorPos(1, 1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\nX", str); }}
test "Terminal: cursorPos relative to origin with left/right" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.scrolling_region.top = 2; t.scrolling_region.bottom = 3; t.scrolling_region.left = 2; t.scrolling_region.right = 4; t.modes.set(.origin, true); t.setCursorPos(1, 1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n X", str); }}
test "Terminal: cursorPos limits with full scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.scrolling_region.top = 2; t.scrolling_region.bottom = 3; t.scrolling_region.left = 2; t.scrolling_region.right = 4; t.modes.set(.origin, true); t.setCursorPos(500, 500); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n\n X", str); }}
// Probably outdated, but dates back to the original terminal implementation.test "Terminal: setCursorPos (original test)" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
// Setting it to 0 should keep it zero (1 based) t.setCursorPos(0, 0); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
// Should clamp to size t.setCursorPos(81, 81); try testing.expectEqual(@as(usize, 79), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 79), t.screens.active.cursor.y);
// Should reset pending wrap t.setCursorPos(0, 80); try t.print('c'); try testing.expect(t.screens.active.cursor.pending_wrap); t.setCursorPos(0, 80); try testing.expect(!t.screens.active.cursor.pending_wrap);
// Origin mode t.modes.set(.origin, true);
// No change without a scroll region t.setCursorPos(81, 81); try testing.expectEqual(@as(usize, 79), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 79), t.screens.active.cursor.y);
// Set the scroll region t.setTopAndBottomMargin(10, t.rows); t.setCursorPos(0, 0); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 9), t.screens.active.cursor.y);
t.setCursorPos(1, 1); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 9), t.screens.active.cursor.y);
t.setCursorPos(100, 0); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 79), t.screens.active.cursor.y);
t.setTopAndBottomMargin(10, 11); t.setCursorPos(2, 0); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 10), t.screens.active.cursor.y);}
test "Terminal: setTopAndBottomMargin simple" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(0, 0);
t.clearDirty(); t.scrollDown(1);
// Mark the rows we moved as dirty. try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); }}
test "Terminal: setTopAndBottomMargin top only" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(2, 0);
t.clearDirty(); t.scrollDown(1);
// This is dirty because the cursor moves from this row try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str); }}
test "Terminal: setTopAndBottomMargin top and bottom" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(1, 2);
t.clearDirty(); t.scrollDown(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nABC\nGHI", str); }}
test "Terminal: setTopAndBottomMargin top equal to bottom" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(2, 2);
t.clearDirty(); t.scrollDown(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); }}
test "Terminal: setLeftAndRightMargin simple" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(0, 0);
t.clearDirty(); t.eraseChars(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" BC\nDEF\nGHI", str); }}
test "Terminal: setLeftAndRightMargin left only" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(2, 0); try testing.expectEqual(@as(usize, 1), t.scrolling_region.left); try testing.expectEqual(@as(usize, t.cols - 1), t.scrolling_region.right); t.setCursorPos(1, 2);
t.clearDirty(); t.insertLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\nDBC\nGEF\n HI", str); }}
test "Terminal: setLeftAndRightMargin left and right" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(1, 2); t.setCursorPos(1, 2);
t.clearDirty(); t.insertLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" C\nABF\nDEI\nGH", str); }}
test "Terminal: setLeftAndRightMargin left equal right" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(2, 2); t.setCursorPos(1, 2);
t.clearDirty(); t.insertLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); }}
test "Terminal: setLeftAndRightMargin mode 69 unset" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, false); t.setLeftAndRightMargin(1, 2); t.setCursorPos(1, 2);
t.clearDirty(); t.insertLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); }}
test "Terminal: insertLines simple" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2);
t.clearDirty(); t.insertLines(1);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str); }}
test "Terminal: insertLines colors with bg color" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2);
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.insertLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str); }
for (0..t.cols) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 1, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); }}
test "Terminal: insertLines handles style refs" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 3 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed();
// For the line being deleted, create a refcounted style try t.setAttribute(.{ .bold = {} }); try t.printString("GHI"); try t.setAttribute(.{ .unset = {} });
// verify we have styles in our style map const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count());
t.setCursorPos(2, 2); t.insertLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\n\nDEF", str); }
// verify we have no styles in our style map try testing.expectEqual(@as(usize, 0), page.styles.count());}
test "Terminal: insertLines outside of scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(3, 4); t.setCursorPos(2, 2);
t.clearDirty(); t.insertLines(1);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nDEF\nGHI", str); }}
test "Terminal: insertLines top/bottom scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.carriageReturn(); try t.linefeed(); try t.printString("123"); t.setTopAndBottomMargin(1, 3); t.setCursorPos(2, 2);
t.clearDirty(); t.insertLines(1);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\n\nDEF\n123", str); }}
test "Terminal: insertLines across page boundary marks all shifted rows dirty" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 }); defer t.deinit(alloc);
const first_page = t.screens.active.pages.pages.first.?; const first_page_nrows = first_page.data.capacity.rows;
// Fill up the first page minus 3 rows for (0..first_page_nrows - 3) |_| try t.linefeed();
// Add content that will cross a page boundary try t.printString("1AAAA"); t.carriageReturn(); try t.linefeed(); try t.printString("2BBBB"); t.carriageReturn(); try t.linefeed(); try t.printString("3CCCC"); t.carriageReturn(); try t.linefeed(); try t.printString("4DDDD"); t.carriageReturn(); try t.linefeed(); try t.printString("5EEEE");
// Verify we now have a second page try testing.expect(first_page.next != null);
t.setCursorPos(1, 1); t.clearDirty(); t.insertLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n1AAAA\n2BBBB\n3CCCC\n4DDDD", str); }}
test "Terminal: insertLines (legacy test)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.carriageReturn(); try t.linefeed(); try t.print('D'); t.carriageReturn(); try t.linefeed(); try t.print('E');
// Move to row 2 t.setCursorPos(2, 1);
// Insert two lines t.insertLines(2);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\n\n\nB\nC", str); }}
test "Terminal: insertLines zero" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
// This should do nothing t.setCursorPos(1, 1); t.insertLines(0);}
test "Terminal: insertLines with scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 6 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.carriageReturn(); try t.linefeed(); try t.print('D'); t.carriageReturn(); try t.linefeed(); try t.print('E');
t.setTopAndBottomMargin(1, 2); t.setCursorPos(1, 1);
t.clearDirty(); t.insertLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X\nA\nC\nD\nE", str); }}
test "Terminal: insertLines more than remaining" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.carriageReturn(); try t.linefeed(); try t.print('D'); t.carriageReturn(); try t.linefeed(); try t.print('E');
// Move to row 2 t.setCursorPos(2, 1);
// Insert a bunch of lines t.clearDirty(); t.insertLines(20);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A", str); }}
test "Terminal: insertLines resets pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.insertLines(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('B');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("B\nABCDE", str); }}
test "Terminal: insertLines resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 3, .cols = 3 }); defer t.deinit(alloc);
try t.print('1'); t.carriageReturn(); try t.linefeed(); for ("ABCDEF") |c| try t.print(c); t.setCursorPos(1, 1); t.insertLines(1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X\n1\nABC", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 2 } }).?; const row = list_cell.row; try testing.expect(!row.wrap); }}
test "Terminal: insertLines multi-codepoint graphemes" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Disable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed();
// This is: 👨👩👧 (which may or may not render correctly) try t.print(0x1F468); try t.print(0x200D); try t.print(0x1F469); try t.print(0x200D); try t.print(0x1F467);
t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); t.insertLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\n\n👨👩👧\nGHI", str); }}
test "Terminal: insertLines left/right scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF456"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2);
t.clearDirty(); t.insertLines(1);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC123\nD 56\nGEF489\n HI7", str); }}
test "Terminal: scrollUp simple" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2);
const cursor = t.screens.active.cursor; const viewport_before = t.screens.active.pages.getTopLeft(.viewport); try t.scrollUp(1); try testing.expectEqual(cursor.x, t.screens.active.cursor.x); try testing.expectEqual(cursor.y, t.screens.active.cursor.y);
// Viewport should have moved. Our entire page should've scrolled! // The viewport moving will cause our render state to make the full // frame as dirty. const viewport_after = t.screens.active.pages.getTopLeft(.viewport); try testing.expect(!viewport_before.eql(viewport_after));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("DEF\nGHI", str); }}
test "Terminal: scrollUp moves hyperlink" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.screens.active.startHyperlink("http://example.com", null); try t.printString("DEF"); t.screens.active.endHyperlink(); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); try t.scrollUp(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("DEF\nGHI", str); }
for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 1, } }).?; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); }}
test "Terminal: scrollUp clears hyperlink" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null); try t.printString("ABC"); t.screens.active.endHyperlink(); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); try t.scrollUp(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("DEF\nGHI", str); }
for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); }}
test "Terminal: scrollUp top/bottom scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(2, 3); t.setCursorPos(1, 1);
t.clearDirty(); try t.scrollUp(1);
// This is dirty because the cursor moves from this row try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nGHI", str); }}
test "Terminal: scrollUp left/right scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF456"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2);
const cursor = t.screens.active.cursor; t.clearDirty(); try t.scrollUp(1); try testing.expectEqual(cursor.x, t.screens.active.cursor.x); try testing.expectEqual(cursor.y, t.screens.active.cursor.y);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str); }}
test "Terminal: scrollUp left/right scroll region hyperlink" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.carriageReturn(); try t.linefeed(); try t.screens.active.startHyperlink("http://example.com", null); try t.printString("DEF456"); t.screens.active.endHyperlink(); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2); try t.scrollUp(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str); }
// First row gets some hyperlinks { for (0..1) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } for (1..4) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (4..6) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } }
// Second row preserves hyperlink where we didn't scroll { for (0..1) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 1, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (1..4) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 1, } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } for (4..6) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 1, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } }}
test "Terminal: scrollUp preserves pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setCursorPos(1, 5); try t.print('A'); t.setCursorPos(2, 5); try t.print('B'); t.setCursorPos(3, 5); try t.print('C'); try t.scrollUp(1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" B\n C\n\nX", str); }}
test "Terminal: scrollUp full top/bottom region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("top"); t.setCursorPos(5, 1); try t.printString("ABCDE"); t.setTopAndBottomMargin(2, 5);
t.clearDirty(); try t.scrollUp(4);
// This is dirty because the cursor moves from this row try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("top", str); }}
test "Terminal: scrollUp full top/bottomleft/right scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("top"); t.setCursorPos(5, 1); try t.printString("ABCDE"); t.modes.set(.enable_left_and_right_margin, true); t.setTopAndBottomMargin(2, 5); t.setLeftAndRightMargin(2, 4);
t.clearDirty(); try t.scrollUp(4);
// This is dirty because the cursor moves from this row try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); for (1..5) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("top\n\n\n\nA E", str); }}
test "Terminal: scrollUp creates scrollback in primary screen" { // When in primary screen with full-width scroll region at top, // scrollUp (CSI S) should push lines into scrollback like xterm. const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 10 }); defer t.deinit(alloc);
// Fill the screen with content try t.printString("AAAAA"); t.carriageReturn(); try t.linefeed(); try t.printString("BBBBB"); t.carriageReturn(); try t.linefeed(); try t.printString("CCCCC"); t.carriageReturn(); try t.linefeed(); try t.printString("DDDDD"); t.carriageReturn(); try t.linefeed(); try t.printString("EEEEE");
t.clearDirty();
// Scroll up by 1, which should push "AAAAA" into scrollback try t.scrollUp(1);
// The cursor row (new empty row) should be dirty try testing.expect(t.screens.active.cursor.page_row.dirty);
// The active screen should now show BBBBB through EEEEE plus one blank line { const str = try t.plainString(alloc); defer alloc.free(str); try testing.expectEqualStrings("BBBBB\nCCCCC\nDDDDD\nEEEEE", str); }
// Now scroll to the top to see scrollback - AAAAA should be there t.screens.active.scroll(.{ .top = {} }); { const str = try t.plainString(alloc); defer alloc.free(str); // Should see AAAAA in scrollback try testing.expectEqualStrings("AAAAA\nBBBBB\nCCCCC\nDDDDD\nEEEEE", str); }}
test "Terminal: scrollUp with max_scrollback zero" { // When max_scrollback is 0, scrollUp should still work but not retain history const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 }); defer t.deinit(alloc);
try t.printString("AAAAA"); t.carriageReturn(); try t.linefeed(); try t.printString("BBBBB"); t.carriageReturn(); try t.linefeed(); try t.printString("CCCCC");
try t.scrollUp(1);
// Active screen should show scrolled content { const str = try t.plainString(alloc); defer alloc.free(str); try testing.expectEqualStrings("BBBBB\nCCCCC", str); }
// Scroll to top - should be same as active since no scrollback t.screens.active.scroll(.{ .top = {} }); { const str = try t.plainString(alloc); defer alloc.free(str); try testing.expectEqualStrings("BBBBB\nCCCCC", str); }}
test "Terminal: scrollUp with max_scrollback zero and top margin" { // When max_scrollback is 0 and top margin is set, should use deleteLines path const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 }); defer t.deinit(alloc);
try t.printString("AAAAA"); t.carriageReturn(); try t.linefeed(); try t.printString("BBBBB"); t.carriageReturn(); try t.linefeed(); try t.printString("CCCCC"); t.carriageReturn(); try t.linefeed(); try t.printString("DDDDD");
// Set top margin (not at row 0) t.setTopAndBottomMargin(2, 5);
try t.scrollUp(1);
{ const str = try t.plainString(alloc); defer alloc.free(str); // First row preserved, rest scrolled try testing.expectEqualStrings("AAAAA\nCCCCC\nDDDDD", str); }}
test "Terminal: scrollUp with max_scrollback zero and left/right margin" { // When max_scrollback is 0 with left/right margins, uses deleteLines path const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 0 }); defer t.deinit(alloc);
try t.printString("AAAAABBBBB"); t.carriageReturn(); try t.linefeed(); try t.printString("CCCCCDDDDD"); t.carriageReturn(); try t.linefeed(); try t.printString("EEEEEFFFFF");
// Set left/right margins (columns 2-6, 1-indexed = indices 1-5) t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(2, 6);
try t.scrollUp(1);
{ const str = try t.plainString(alloc); defer alloc.free(str); // cols 1-5 scroll, col 0 and cols 6+ preserved try testing.expectEqualStrings("ACCCCDBBBB\nCEEEEFDDDD\nE FFFF", str); }}
test "Terminal: scrollDown simple" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2);
const cursor = t.screens.active.cursor; t.clearDirty(); t.scrollDown(1); try testing.expectEqual(cursor.x, t.screens.active.cursor.x); try testing.expectEqual(cursor.y, t.screens.active.cursor.y);
for (0..5) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); }}
test "Terminal: scrollDown hyperlink moves" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null); try t.printString("ABC"); t.screens.active.endHyperlink(); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); t.scrollDown(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nABC\nDEF\nGHI", str); }
for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 1, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); }}
test "Terminal: scrollDown outside of scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(3, 4); t.setCursorPos(2, 2);
const cursor = t.screens.active.cursor; t.clearDirty(); t.scrollDown(1); try testing.expectEqual(cursor.x, t.screens.active.cursor.x); try testing.expectEqual(cursor.y, t.screens.active.cursor.y);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
// This is dirty because the cursor moves from this row try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nDEF\n\nGHI", str); }}
test "Terminal: scrollDown left/right scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF456"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2);
const cursor = t.screens.active.cursor; t.clearDirty(); t.scrollDown(1); try testing.expectEqual(cursor.x, t.screens.active.cursor.x); try testing.expectEqual(cursor.y, t.screens.active.cursor.y);
for (0..4) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A 23\nDBC156\nGEF489\n HI7", str); }}
test "Terminal: scrollDown left/right scroll region hyperlink" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null); try t.printString("ABC123"); t.screens.active.endHyperlink(); t.carriageReturn(); try t.linefeed(); try t.printString("DEF456"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2); t.scrollDown(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A 23\nDBC156\nGEF489\n HI7", str); }
// First row preserves hyperlink where we didn't scroll { for (0..1) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (1..4) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } for (4..6) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } }
// Second row gets some hyperlinks { for (0..1) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 1, } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } for (1..4) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 1, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); const page = &list_cell.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); } for (4..6) |x| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(x), .y = 1, } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); } }}
test "Terminal: scrollDown outside of left/right scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF456"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(1, 1);
const cursor = t.screens.active.cursor; t.clearDirty(); t.scrollDown(1); try testing.expectEqual(cursor.x, t.screens.active.cursor.x); try testing.expectEqual(cursor.y, t.screens.active.cursor.y);
for (0..4) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A 23\nDBC156\nGEF489\n HI7", str); }}
test "Terminal: scrollDown preserves pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 10 }); defer t.deinit(alloc);
t.setCursorPos(1, 5); try t.print('A'); t.setCursorPos(2, 5); try t.print('B'); t.setCursorPos(3, 5); try t.print('C'); t.scrollDown(1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n A\n B\nX C", str); }}
test "Terminal: eraseChars simple operation" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); t.clearDirty(); t.eraseChars(2); try t.print('X');
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X C", str); }}
test "Terminal: eraseChars minimum one" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); t.clearDirty(); t.eraseChars(0); try t.print('X'); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("XBC", str); }}
test "Terminal: eraseChars beyond screen edge" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for (" ABC") |c| try t.print(c); t.setCursorPos(1, 4); t.eraseChars(10);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" A", str); }}
test "Terminal: eraseChars wide character" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.print('橋'); for ("BC") |c| try t.print(c); t.setCursorPos(1, 1); t.eraseChars(1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X BC", str); }}
test "Terminal: eraseChars resets pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.eraseChars(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDX", str); }}
test "Terminal: eraseChars resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE123") |c| try t.print(c); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; const row = list_cell.row; try testing.expect(row.wrap); }
t.setCursorPos(1, 1); t.eraseChars(1);
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; const row = list_cell.row; try testing.expect(!row.wrap); }
try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("XBCDE\n123", str); }}
test "Terminal: eraseChars preserves background sgr" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.eraseChars(2);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" C", str); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); } { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 1, .y = 0 } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); } }}
test "Terminal: eraseChars handles refcounted styles" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} }); try t.print('A'); try t.print('B'); try t.setAttribute(.{ .unset = {} }); try t.print('C');
// verify we have styles in our style map const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count());
t.setCursorPos(1, 1); t.eraseChars(2);
// verify we have no styles in our style map try testing.expectEqual(@as(usize, 0), page.styles.count());}
test "Terminal: eraseChars protected attributes respected with iso" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); t.eraseChars(2);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC", str); }}
test "Terminal: eraseChars protected attributes ignored with dec most recent" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(1, 1); t.eraseChars(2);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" C", str); }}
test "Terminal: eraseChars protected attributes ignored with dec set" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); t.eraseChars(2);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" C", str); }}
test "Terminal: eraseChars wide char boundary conditions" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 1, .cols = 8 }); defer t.deinit(alloc);
try t.printString("😀a😀b😀"); { const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings("😀a😀b😀", str); }
t.setCursorPos(1, 2); t.eraseChars(3); t.screens.active.cursor.page_pin.node.data.assertIntegrity();
{ const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings(" b😀", str); }}
test "Terminal: eraseChars wide char splits proper cell boundaries" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 1, .cols = 30 }); defer t.deinit(alloc);
// This is a test for a bug: https://github.com/ghostty-org/ghostty/issues/2817 // To explain the setup: // (1) We need our wide characters starting on an even (1-based) column. // (2) We need our cursor to be in the middle somewhere. // (3) We need our count to be less than our cursor X and on a split cell. // The bug was that we split the wrong cell boundaries.
try t.printString("x食べて下さい"); { const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings("x食べて下さい", str); }
t.setCursorPos(1, 6); // At: て t.eraseChars(4); // Delete: て下 t.screens.active.cursor.page_pin.node.data.assertIntegrity();
{ const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings("x食べ さい", str); }}
test "Terminal: eraseChars wide char wrap boundary conditions" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 3, .cols = 8 }); defer t.deinit(alloc);
try t.printString(".......😀abcde😀......"); { const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings(".......\n😀abcde\n😀......", str);
const unwrapped = try t.plainStringUnwrapped(alloc); defer testing.allocator.free(unwrapped); try testing.expectEqualStrings(".......😀abcde😀......", unwrapped); }
t.setCursorPos(2, 2); t.eraseChars(3); t.screens.active.cursor.page_pin.node.data.assertIntegrity();
{ const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings(".......\n cde\n😀......", str);
const unwrapped = try t.plainStringUnwrapped(alloc); defer testing.allocator.free(unwrapped); try testing.expectEqualStrings("....... cde\n😀......", unwrapped); }}
test "Terminal: reverseIndex" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.reverseIndex(); try t.print('D'); t.carriageReturn(); try t.linefeed(); t.carriageReturn(); try t.linefeed();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\nBD\nC", str); }}
test "Terminal: reverseIndex from the top" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); t.carriageReturn(); try t.linefeed();
t.setCursorPos(1, 1); t.reverseIndex(); try t.print('D');
t.carriageReturn(); try t.linefeed(); t.setCursorPos(1, 1); t.reverseIndex(); try t.print('E'); t.carriageReturn(); try t.linefeed();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("E\nD\nA\nB", str); }}
test "Terminal: reverseIndex top of scrolling region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 10 }); defer t.deinit(alloc);
// Initial value t.setCursorPos(2, 1); try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.carriageReturn(); try t.linefeed(); try t.print('D'); t.carriageReturn(); try t.linefeed();
// Set our scroll region t.setTopAndBottomMargin(2, 5); t.setCursorPos(2, 1); t.reverseIndex(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nX\nA\nB\nC", str); }}
test "Terminal: reverseIndex top of screen" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.print('A'); t.setCursorPos(2, 1); try t.print('B'); t.setCursorPos(3, 1); try t.print('C'); t.setCursorPos(1, 1); t.reverseIndex(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X\nA\nB\nC", str); }}
test "Terminal: reverseIndex not top of screen" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.print('A'); t.setCursorPos(2, 1); try t.print('B'); t.setCursorPos(3, 1); try t.print('C'); t.setCursorPos(2, 1); t.reverseIndex(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X\nB\nC", str); }}
test "Terminal: reverseIndex top/bottom margins" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.print('A'); t.setCursorPos(2, 1); try t.print('B'); t.setCursorPos(3, 1); try t.print('C'); t.setTopAndBottomMargin(2, 3); t.setCursorPos(2, 1); t.reverseIndex();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\n\nB", str); }}
test "Terminal: reverseIndex outside top/bottom margins" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.print('A'); t.setCursorPos(2, 1); try t.print('B'); t.setCursorPos(3, 1); try t.print('C'); t.setTopAndBottomMargin(2, 3); t.setCursorPos(1, 1); t.reverseIndex();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\nB\nC", str); }}
test "Terminal: reverseIndex left/right margins" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.setCursorPos(2, 1); try t.printString("DEF"); t.setCursorPos(3, 1); try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(2, 3); t.setCursorPos(1, 2); t.reverseIndex();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\nDBC\nGEF\n HI", str); }}
test "Terminal: reverseIndex outside left/right margins" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.setCursorPos(2, 1); try t.printString("DEF"); t.setCursorPos(3, 1); try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(2, 3); t.setCursorPos(1, 1); t.reverseIndex();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nDEF\nGHI", str); }}
test "Terminal: index" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
try t.index(); try t.print('A');
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nA", str); }}
test "Terminal: index from the bottom" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
t.setCursorPos(5, 1); try t.print('A'); t.cursorLeft(1); // undo moving right from 'A'
t.clearDirty(); try t.index(); try t.print('B');
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n\nA\nB", str); }}
test "Terminal: index scrolling with hyperlink" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
t.setCursorPos(5, 1); try t.screens.active.startHyperlink("http://example.com", null); try t.print('A'); t.screens.active.endHyperlink(); t.cursorLeft(1); // undo moving right from 'A' try t.index(); try t.print('B');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n\nA\nB", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = 0, .y = 3, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = 0, .y = 4, } }).?; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); }}
test "Terminal: index outside of scrolling region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); t.setTopAndBottomMargin(2, 5); try t.index(); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);}
test "Terminal: index from the bottom outside of scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 2); t.setCursorPos(5, 1); try t.print('A'); t.clearDirty(); try t.index(); try t.print('B'); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n\n\nAB", str); }}
test "Terminal: index no scroll region, top of screen" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.print('A'); t.clearDirty(); try t.index(); try t.print('X');
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\n X", str); }}
test "Terminal: index bottom of primary screen" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setCursorPos(5, 1); try t.print('A'); t.clearDirty(); try t.index(); try t.print('X');
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n\nA\n X", str); }}
test "Terminal: index bottom of primary screen background sgr" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setCursorPos(5, 1); try t.print('A'); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); try t.index();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n\nA", str); for (0..5) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 4, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); } }}
test "Terminal: index inside scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); try t.print('A'); t.clearDirty(); try t.index(); try t.print('X');
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\n X", str); }}
test "Terminal: index bottom of scroll region with hyperlinks" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 2); try t.print('A'); try t.index(); t.carriageReturn(); try t.screens.active.startHyperlink("http://example.com", null); try t.print('B'); t.screens.active.endHyperlink(); try t.index(); t.carriageReturn(); try t.print('C');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("B\nC", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = 0, .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = 0, .y = 1, } }).?; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); }}
test "Terminal: index bottom of scroll region clear hyperlinks" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(2, 3); t.setCursorPos(2, 1); try t.screens.active.startHyperlink("http://example.com", null); try t.print('A'); t.screens.active.endHyperlink(); try t.index(); t.carriageReturn(); try t.print('B'); try t.index(); t.carriageReturn(); try t.print('C');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nB\nC", str); }
for (1..3) |y| { const list_cell = t.screens.active.pages.getCell(.{ .viewport = .{ .x = 0, .y = @intCast(y), } }).?; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); const page = &list_cell.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); }}
test "Terminal: index bottom of scroll region with background SGR" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); t.setCursorPos(4, 1); try t.print('B'); t.setCursorPos(3, 1); try t.print('A'); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); try t.index();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nA\n\nB", str); }
for (0..t.cols) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 2, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); }}
test "Terminal: index bottom of primary screen with scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); t.setCursorPos(3, 1); try t.print('A'); t.setCursorPos(5, 1); t.clearDirty(); try t.index(); try t.index(); try t.index(); try t.print('X');
for (0..4) |y| try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\nA\n\nX", str); }}
test "Terminal: index outside left/right margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); t.scrolling_region.left = 3; t.scrolling_region.right = 5; t.setCursorPos(3, 3); try t.print('A'); t.setCursorPos(3, 1); t.clearDirty(); try t.index(); try t.print('X');
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\nX A", str); }}
test "Terminal: index inside left/right margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
try t.printString("AAAAAA"); t.carriageReturn(); try t.linefeed(); try t.printString("AAAAAA"); t.carriageReturn(); try t.linefeed(); try t.printString("AAAAAA"); t.modes.set(.enable_left_and_right_margin, true); t.setTopAndBottomMargin(1, 3); t.setLeftAndRightMargin(1, 3); t.setCursorPos(3, 1);
t.clearDirty(); try t.index();
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AAAAAA\nAAAAAA\n AAA", str); }}
test "Terminal: index bottom of scroll region creates scrollback" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); try t.printString("1\n2\n3"); t.setCursorPos(4, 1); try t.print('X'); t.setCursorPos(3, 1); try t.index(); try t.print('Y');
{ const str = try t.screens.active.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer testing.allocator.free(str); try testing.expectEqualStrings("2\n3\nY\nX", str); } { const str = try t.screens.active.dumpStringAlloc(alloc, .{ .screen = .{} }); defer testing.allocator.free(str); try testing.expectEqualStrings("1\n2\n3\nY\nX", str); }}
test "Terminal: index bottom of scroll region no scrollback" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); t.setCursorPos(4, 1); try t.print('B'); t.setCursorPos(3, 1); try t.print('A'); t.clearDirty(); try t.index(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nA\n X\nB", str); }}
test "Terminal: index bottom of scroll region blank line preserves SGR" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); try t.printString("1\n2\n3"); t.setCursorPos(4, 1); try t.print('X'); t.setCursorPos(3, 1); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); try t.index();
{ const str = try t.screens.active.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer testing.allocator.free(str); try testing.expectEqualStrings("2\n3\n\nX", str); } { const str = try t.screens.active.dumpStringAlloc(alloc, .{ .screen = .{} }); defer testing.allocator.free(str); try testing.expectEqualStrings("1\n2\n3\n\nX", str); } for (0..t.cols) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 2, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); }}
test "Terminal: cursorUp basic" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setCursorPos(3, 1); try t.print('A'); t.cursorUp(10); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X\n\nA", str); }}
test "Terminal: cursorUp below top scroll margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(2, 4); t.setCursorPos(3, 1); try t.print('A'); t.cursorUp(5); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n X\nA", str); }}
test "Terminal: cursorUp above top scroll margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(3, 5); t.setCursorPos(3, 1); try t.print('A'); t.setCursorPos(2, 1); t.cursorUp(10); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X\n\nA", str); }}
test "Terminal: cursorUp resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.cursorUp(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDX", str); }}
test "Terminal: cursorLeft no wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.cursorLeft(10);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\nB", str); }}
test "Terminal: cursorLeft unsets pending wrap state" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.cursorLeft(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCXE", str); }}
test "Terminal: cursorLeft unsets pending wrap state with longer jump" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.cursorLeft(3); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AXCDE", str); }}
test "Terminal: cursorLeft reverse wrap with pending wrap state" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap, true);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.cursorLeft(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDX", str); }}
test "Terminal: cursorLeft reverse wrap extended with pending wrap state" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap_extended, true);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.cursorLeft(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDX", str); }}
test "Terminal: cursorLeft reverse wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap, true);
for ("ABCDE1") |c| try t.print(c); t.cursorLeft(2); try t.print('X'); try testing.expect(t.screens.active.cursor.pending_wrap);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDX\n1", str); }}
test "Terminal: cursorLeft reverse wrap with no soft wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap, true);
for ("ABCDE") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); try t.print('1'); t.cursorLeft(2); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDE\nX", str); }}
test "Terminal: cursorLeft reverse wrap before left margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap, true); t.setTopAndBottomMargin(3, 0); t.cursorLeft(1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n\nX", str); }}
test "Terminal: cursorLeft extended reverse wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap_extended, true);
for ("ABCDE") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); try t.print('1'); t.cursorLeft(2); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDX\n1", str); }}
test "Terminal: cursorLeft extended reverse wrap bottom wraparound" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 3 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap_extended, true);
for ("ABCDE") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); try t.print('1'); t.cursorLeft(1 + t.cols + 1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDE\n1\n X", str); }}
test "Terminal: cursorLeft extended reverse wrap is priority if both set" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 3 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap, true); t.modes.set(.reverse_wrap_extended, true);
for ("ABCDE") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); try t.print('1'); t.cursorLeft(1 + t.cols + 1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDE\n1\n X", str); }}
test "Terminal: cursorLeft extended reverse wrap above top scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap_extended, true);
t.setTopAndBottomMargin(3, 0); t.setCursorPos(2, 1); t.cursorLeft(1000);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);}
test "Terminal: cursorLeft reverse wrap on first row" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); t.modes.set(.reverse_wrap, true);
t.setTopAndBottomMargin(3, 0); t.setCursorPos(1, 2); t.cursorLeft(1000);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);}
test "Terminal: cursorDown basic" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.print('A'); t.cursorDown(10); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\n\n\n\n X", str); }}
test "Terminal: cursorDown above bottom scroll margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); try t.print('A'); t.cursorDown(10); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\n\n X", str); }}
test "Terminal: cursorDown below bottom scroll margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3); try t.print('A'); t.setCursorPos(4, 1); t.cursorDown(10); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\n\n\n\nX", str); }}
test "Terminal: cursorDown resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.cursorDown(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDE\n X", str); }}
test "Terminal: cursorRight resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.cursorRight(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDX", str); }}
test "Terminal: cursorRight to the edge of screen" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.cursorRight(100); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); }}
test "Terminal: cursorRight left of right margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.scrolling_region.right = 2; t.cursorRight(100); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); }}
test "Terminal: cursorRight right of right margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.scrolling_region.right = 2; t.setCursorPos(1, 4); t.cursorRight(100); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); }}
test "Terminal: deleteLines simple" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2);
t.clearDirty(); t.deleteLines(1);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nGHI", str); }}
test "Terminal: deleteLines colors with bg color" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("ABC"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2);
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.deleteLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nGHI", str); }
for (0..t.cols) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 4, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); }}
test "Terminal: deleteLines across page boundary marks all shifted rows dirty" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 }); defer t.deinit(alloc);
const first_page = t.screens.active.pages.pages.first.?; const first_page_nrows = first_page.data.capacity.rows;
// Fill up the first page minus 3 rows for (0..first_page_nrows - 3) |_| try t.linefeed();
// Add content that will cross a page boundary try t.printString("1AAAA"); t.carriageReturn(); try t.linefeed(); try t.printString("2BBBB"); t.carriageReturn(); try t.linefeed(); try t.printString("3CCCC"); t.carriageReturn(); try t.linefeed(); try t.printString("4DDDD"); t.carriageReturn(); try t.linefeed(); try t.printString("5EEEE");
// Verify we now have a second page try testing.expect(first_page.next != null);
t.setCursorPos(1, 1); t.clearDirty(); t.deleteLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("2BBBB\n3CCCC\n4DDDD\n5EEEE", str); }}
test "Terminal: deleteLines (legacy)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.carriageReturn(); try t.linefeed(); try t.print('D');
t.cursorUp(2); t.deleteLines(1);
try t.print('E'); t.carriageReturn(); try t.linefeed();
// We should be try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.y);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\nE\nD", str); }}
test "Terminal: deleteLines with scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.carriageReturn(); try t.linefeed(); try t.print('D');
t.setTopAndBottomMargin(1, 3); t.setCursorPos(1, 1);
t.clearDirty(); t.deleteLines(1);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
try t.print('E'); t.carriageReturn(); try t.linefeed();
// We should be // try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); // try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.y);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("E\nC\n\nD", str); }}
test "Terminal: deleteLines with scroll region, large count" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.carriageReturn(); try t.linefeed(); try t.print('D');
t.setTopAndBottomMargin(1, 3); t.setCursorPos(1, 1);
t.clearDirty(); t.deleteLines(5);
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
try t.print('E'); t.carriageReturn(); try t.linefeed();
// We should be // try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); // try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.y);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("E\n\n\nD", str); }}
test "Terminal: deleteLines with scroll region, cursor outside of region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); t.carriageReturn(); try t.linefeed(); try t.print('C'); t.carriageReturn(); try t.linefeed(); try t.print('D');
t.setTopAndBottomMargin(1, 3); t.setCursorPos(4, 1);
t.clearDirty(); t.deleteLines(1);
for (0..4) |y| try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\nB\nC\nD", str); }}
test "Terminal: deleteLines resets pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.deleteLines(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('B');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("B", str); }}
test "Terminal: deleteLines resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 3, .cols = 3 }); defer t.deinit(alloc);
try t.print('1'); t.carriageReturn(); try t.linefeed(); for ("ABCDEF") |c| try t.print(c);
t.setTopAndBottomMargin(1, 2); t.setCursorPos(1, 1); t.deleteLines(1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("XBC\n\nDEF", str); }
for (0..t.rows) |y| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = @intCast(y), } }).?; const row = list_cell.row; try testing.expect(!row.wrap); }}
test "Terminal: deleteLines left/right scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF456"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2);
t.clearDirty(); t.deleteLines(1);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); for (1..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC123\nDHI756\nG 89", str); }}
test "Terminal: deleteLines left/right scroll region from top" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF456"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(1, 2);
t.clearDirty(); t.deleteLines(1);
for (0..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str); }}
test "Terminal: deleteLines left/right scroll region high count" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.carriageReturn(); try t.linefeed(); try t.printString("DEF456"); t.carriageReturn(); try t.linefeed(); try t.printString("GHI789"); t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2);
t.clearDirty(); t.deleteLines(100);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); for (1..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC123\nD 56\nG 89", str); }}
test "Terminal: deleteLines wide character spacer head" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 3 }); defer t.deinit(alloc);
// Initial value // +-----+ // |AAAAA| < Wrapped // |BBBB*| < Wrapped (continued) // |WWCCC| < Non-wrapped (continued) // +-----+ // where * represents a spacer head cell // and WW is the wide character. try t.printString("AAAAABBBB\u{1F600}CCC");
// Delete the top line // +-----+ // |BBBB | < Non-wrapped // |WWCCC| < Non-wrapped // | | < Non-wrapped // +-----+ // This should convert the spacer head to // a regular empty cell, and un-set wrap. t.setCursorPos(1, 1); t.deleteLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); defer testing.allocator.free(unwrapped_str); try testing.expectEqualStrings("BBBB\n\u{1F600}CCC", str); try testing.expectEqualStrings("BBBB\n\u{1F600}CCC", unwrapped_str); }}
test "Terminal: deleteLines wide character spacer head left scroll margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 3 }); defer t.deinit(alloc);
// Initial value // +-----+ // |AAAAA| < Wrapped // |BBBB*| < Wrapped (continued) // |WWCCC| < Non-wrapped (continued) // +-----+ // where * represents a spacer head cell // and WW is the wide character. try t.printString("AAAAABBBB\u{1F600}CCC");
t.scrolling_region.left = 2;
// Delete the top line // ### <- scrolling region // +-----+ // |AABB | < Wrapped // |BBCCC| < Wrapped (continued) // |WW | < Non-wrapped (continued) // +-----+ // This should convert the spacer head to // a regular empty cell, but due to the // left scrolling margin, wrap state should // remain. t.setCursorPos(1, 3); t.deleteLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); defer testing.allocator.free(unwrapped_str); try testing.expectEqualStrings("AABB\nBBCCC\n\u{1F600}", str); try testing.expectEqualStrings("AABB BBCCC\u{1F600}", unwrapped_str); }}
test "Terminal: deleteLines wide character spacer head right scroll margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 3 }); defer t.deinit(alloc);
// Initial value // +-----+ // |AAAAA| < Wrapped // |BBBB*| < Wrapped (continued) // |WWCCC| < Non-wrapped (continued) // +-----+ // where * represents a spacer head cell // and WW is the wide character. try t.printString("AAAAABBBB\u{1F600}CCC");
t.scrolling_region.right = 3;
// Delete the top line // #### <- scrolling region // +-----+ // |BBBBA| < Wrapped // |WWCC | < Wrapped (continued) // | C| < Non-wrapped (continued) // +-----+ // This should convert the spacer head to // a regular empty cell, but due to the // right scrolling margin, wrap state should // remain. t.setCursorPos(1, 1); t.deleteLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); defer testing.allocator.free(unwrapped_str); try testing.expectEqualStrings("BBBBA\n\u{1F600}CC\n C", str); try testing.expectEqualStrings("BBBBA\u{1F600}CC C", unwrapped_str); }}
test "Terminal: deleteLines wide character spacer head left and right scroll margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 3 }); defer t.deinit(alloc);
// Initial value // +-----+ // |AAAAA| < Wrapped // |BBBB*| < Wrapped (continued) // |WWCCC| < Non-wrapped (continued) // +-----+ // where * represents a spacer head cell // and WW is the wide character. try t.printString("AAAAABBBB\u{1F600}CCC");
t.scrolling_region.right = 3; t.scrolling_region.left = 2;
// Delete the top line // ## <- scrolling region // +-----+ // |AABBA| < Wrapped // |BBCC*| < Wrapped (continued) // |WW C| < Non-wrapped (continued) // +-----+ // Because there is both a left scrolling // margin > 1 and a right scrolling margin // the spacer head should remain, and the // wrap state should be untouched. t.setCursorPos(1, 3); t.deleteLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); defer testing.allocator.free(unwrapped_str); try testing.expectEqualStrings("AABBA\nBBCC\n\u{1F600} C", str); try testing.expectEqualStrings("AABBABBCC\u{1F600} C", unwrapped_str); }}
test "Terminal: deleteLines wide character spacer head left (< 2) and right scroll margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 3 }); defer t.deinit(alloc);
// Initial value // +-----+ // |AAAAA| < Wrapped // |BBBB*| < Wrapped (continued) // |WWCCC| < Non-wrapped (continued) // +-----+ // where * represents a spacer head cell // and WW is the wide character. try t.printString("AAAAABBBB\u{1F600}CCC");
t.scrolling_region.right = 3; t.scrolling_region.left = 1;
// Delete the top line // ### <- scrolling region // +-----+ // |ABBBA| < Wrapped // |B CC | < Wrapped (continued) // | C| < Non-wrapped (continued) // +-----+ // Because the left margin is 1, the wide // char is split, and therefore removed, // along with the spacer head - however, // wrap state should be untouched. t.setCursorPos(1, 2); t.deleteLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); defer testing.allocator.free(unwrapped_str); try testing.expectEqualStrings("ABBBA\nB CC\n C", str); try testing.expectEqualStrings("ABBBAB CC C", unwrapped_str); }}
test "Terminal: deleteLines wide characters split by left/right scroll region boundaries" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 2 }); defer t.deinit(alloc);
// Initial value // +-----+ // |AAAAA| // |WWBWW| // +-----+ // where WW represents a wide character try t.printString("AAAAA\n\u{1F600}B\u{1F600}");
t.scrolling_region.right = 3; t.scrolling_region.left = 1;
// Delete the top line // ### <- scrolling region // +-----+ // |A B A| // | | // +-----+ // The two wide chars, because they're // split by the edge of the scrolling // region, get removed. t.setCursorPos(1, 2); t.deleteLines(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A B A", str); }}
test "Terminal: deleteLines zero" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); defer t.deinit(alloc);
// This should do nothing t.setCursorPos(1, 1); t.deleteLines(0);}
test "Terminal: default style is empty" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.print('A');
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); try testing.expectEqual(@as(style.Id, 0), cell.style_id); }}
test "Terminal: bold style" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} }); try t.print('A');
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); try testing.expect(cell.style_id != 0); const page = &t.screens.active.cursor.page_pin.node.data; try testing.expect(page.styles.refCount(page.memory, t.screens.active.cursor.style_id) > 1); }}
test "Terminal: garbage collect overwritten" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} }); try t.print('A'); t.setCursorPos(1, 1); try t.setAttribute(.{ .unset = {} }); try t.print('B');
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'B'), cell.content.codepoint); try testing.expect(cell.style_id == 0); }
// verify we have no styles in our style map const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count());}
test "Terminal: do not garbage collect old styles in use" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} }); try t.print('A'); try t.setAttribute(.{ .unset = {} }); try t.print('B');
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 'B'), cell.content.codepoint); try testing.expect(cell.style_id == 0); }
// verify we have no styles in our style map const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count());}
test "Terminal: print with style marks the row as styled" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} }); try t.print('A'); try t.setAttribute(.{ .unset = {} }); try t.print('B');
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; try testing.expect(list_cell.row.styled); }}
test "Terminal: DECALN" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 2 }); defer t.deinit(alloc);
// Initial value try t.print('A'); t.carriageReturn(); try t.linefeed(); try t.print('B'); try t.decaln();
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("EE\nEE", str); }}
test "Terminal: decaln reset margins" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); defer t.deinit(alloc);
// Initial value t.modes.set(.origin, true); t.setTopAndBottomMargin(2, 3); try t.decaln(); t.scrollDown(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nEEE\nEEE", str); }}
test "Terminal: decaln preserves color" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); defer t.deinit(alloc);
// Initial value try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0 } }); t.modes.set(.origin, true); t.setTopAndBottomMargin(2, 3); try t.decaln(); t.scrollDown(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\nEEE\nEEE", str); }
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); }}
test "Terminal: DECALN resets graphemes with protected mode" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); defer t.deinit(alloc);
// Add protected mode. A previous version of DECALN accidentally preserved // protected mode which left dangling managed memory. t.setProtectedMode(.iso);
// This is: 👨👩👧 (which may or may not render correctly) t.modes.set(.grapheme_cluster, true); try t.print(0x1F468); try t.print(0x200D); try t.print(0x1F469); try t.print(0x200D); try t.print(0x1F467);
try t.decaln();
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.protected); try testing.expect(t.screens.active.protected_mode == .iso);
for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("EEE\nEEE\nEEE", str); }}
test "Terminal: insertBlanks zero" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 2 }); defer t.deinit(alloc);
try t.print('A'); try t.print('B'); try t.print('C'); t.setCursorPos(1, 1);
t.insertBlanks(0);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC", str); }}
test "Terminal: insertBlanks" { // NOTE: this is not verified with conformance tests, so these // tests might actually be verifying wrong behavior. const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 2 }); defer t.deinit(alloc);
try t.print('A'); try t.print('B'); try t.print('C'); t.setCursorPos(1, 1);
t.clearDirty(); t.insertBlanks(2); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" ABC", str); }}
test "Terminal: insertBlanks pushes off end" { // NOTE: this is not verified with conformance tests, so these // tests might actually be verifying wrong behavior. const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 2 }); defer t.deinit(alloc);
try t.print('A'); try t.print('B'); try t.print('C'); t.setCursorPos(1, 1);
t.clearDirty(); t.insertBlanks(2); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" A", str); }}
test "Terminal: insertBlanks more than size" { // NOTE: this is not verified with conformance tests, so these // tests might actually be verifying wrong behavior. const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 2 }); defer t.deinit(alloc);
try t.print('A'); try t.print('B'); try t.print('C'); t.setCursorPos(1, 1);
t.clearDirty(); t.insertBlanks(5); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }}
test "Terminal: insertBlanks no scroll region, fits" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1);
t.clearDirty(); t.insertBlanks(2); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" ABC", str); }}
test "Terminal: insertBlanks preserves background sgr" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.insertBlanks(2);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" ABC", str); } { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); }}
test "Terminal: insertBlanks shift off screen" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 10 }); defer t.deinit(alloc);
for (" ABC") |c| try t.print(c); t.setCursorPos(1, 3); t.clearDirty(); t.insertBlanks(2); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X A", str); }}
test "Terminal: insertBlanks split multi-cell character" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 10 }); defer t.deinit(alloc);
for ("123") |c| try t.print(c); try t.print('橋'); t.setCursorPos(1, 1); t.clearDirty(); t.insertBlanks(1); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" 123", str); }}
test "Terminal: insertBlanks inside left/right scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
t.scrolling_region.left = 2; t.scrolling_region.right = 4; t.setCursorPos(1, 3); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 3);
t.clearDirty(); t.insertBlanks(2); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X A", str); }}
test "Terminal: insertBlanks outside left/right scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 6, .rows = 10 }); defer t.deinit(alloc);
t.setCursorPos(1, 4); for ("ABC") |c| try t.print(c); t.scrolling_region.left = 2; t.scrolling_region.right = 4; try testing.expect(t.screens.active.cursor.pending_wrap); t.clearDirty(); t.insertBlanks(2); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" ABX", str); }}
test "Terminal: insertBlanks left/right scroll region large count" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
t.modes.set(.origin, true); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setCursorPos(1, 1); t.clearDirty(); t.insertBlanks(140); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); }}
test "Terminal: insertBlanks deleting graphemes" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Disable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.printString("ABC");
// This is: 👨👩👧 (which may or may not render correctly) try t.print(0x1F468); try t.print(0x200D); try t.print(0x1F469); try t.print(0x200D); try t.print(0x1F467);
// We should have one cell with graphemes const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount());
t.setCursorPos(1, 1); t.clearDirty(); t.insertBlanks(4); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" A", str); }
// We should have no graphemes try testing.expectEqual(@as(usize, 0), page.graphemeCount());}
test "Terminal: insertBlanks shift graphemes" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Enable grapheme clustering t.modes.set(.grapheme_cluster, true);
try t.printString("A");
// This is: 👨👩👧 (which may or may not render correctly) try t.print(0x1F468); try t.print(0x200D); try t.print(0x1F469); try t.print(0x200D); try t.print(0x1F467);
// We should have one cell with graphemes const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.graphemeCount());
t.setCursorPos(1, 1); t.clearDirty(); t.insertBlanks(1); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" A👨👩👧", str); }
// We should have no graphemes try testing.expectEqual(@as(usize, 1), page.graphemeCount());}
test "Terminal: insertBlanks split multi-cell character from tail" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 10 }); defer t.deinit(alloc);
try t.printString("橋123"); t.setCursorPos(1, 2); t.insertBlanks(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" 12", str); }}
test "Terminal: insertBlanks shifts hyperlinks" { // osc "8;;http://example.com" // printf "link" // printf "\r" // csi "3@" // echo // // link should be preserved, blanks should not be linked
const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 2 }); defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null); try t.printString("ABC"); t.setCursorPos(1, 1); t.insertBlanks(2);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" ABC", str); }
// Verify all our cells have a hyperlink for (2..5) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(row.hyperlink); const cell = list_cell.cell; try testing.expect(cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell).?; try testing.expectEqual(@as(hyperlink.Id, 1), id); } for (0..2) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); }}
test "Terminal: insertBlanks pushes hyperlink off end completely" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 2 }); defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null); try t.printString("ABC"); t.setCursorPos(1, 1); t.insertBlanks(3);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }
for (0..3) |x| { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = @intCast(x), .y = 0, } }).?; const row = list_cell.row; try testing.expect(!row.hyperlink); const cell = list_cell.cell; try testing.expect(!cell.hyperlink); const id = list_cell.node.data.lookupHyperlink(cell); try testing.expect(id == null); }}
test "Terminal: insertBlanks wide char straddling right margin" { // Crash found by AFL++ fuzzer. // // When a wide character straddles the right scroll margin (head at the // margin, spacer_tail just beyond it), insertBlanks shifts the wide head // away via swapCells but leaves the orphaned spacer_tail in place, // causing a page integrity violation. const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Fill row: A B C D 橋 _ _ _ _ _ // Positions: 0 1 2 3 4W 5T 6 7 8 9 t.setCursorPos(1, 1); for ("ABCD") |c| try t.print(c); try t.print('橋'); // wide char: head at 4, spacer_tail at 5
// Set right margin so the wide head is AT the boundary and the // spacer_tail is just outside it. t.scrolling_region.right = 4;
// Position cursor at x=2 (1-indexed col 3) and insert one blank. // This triggers the swap loop which displaces the wide head at // position 4 without clearing the spacer_tail at position 5. t.setCursorPos(1, 3); t.insertBlanks(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AB CD", str); }}
test "Terminal: insertBlanks wide char spacer_tail orphaned beyond right margin" { // Regression test for AFL++ crash. // // When insertBlanks clears the entire region from cursor to the right // margin (scroll_amount == 0), a wide character whose head is AT the // right margin gets cleared but its spacer_tail just beyond the margin // is left behind, causing a page integrity violation: // "spacer tail not following wide" const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Fill cols 0–9 with wide chars: 中中中中中 // Positions: 0W 1T 2W 3T 4W 5T 6W 7T 8W 9T for (0..5) |_| try t.print(0x4E2D);
// Set left/right margins so that the last wide char (cols 8–9) // straddles the boundary: head at col 8 (inside), tail at col 9 (outside). t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(1, 9); // 1-indexed: left=0, right=8
// Cursor is now at (0, 0) after DECSLRM. Print a narrow char to // advance cursor to col 1. try t.print('a');
// ICH 8: insert 8 blanks at cursor x=1. // rem = right(8) - x(1) + 1 = 8, adjusted_count = 8, scroll_amount = 0. // The code clears cols 1–8 without noticing the spacer_tail at col 9. t.insertBlanks(8);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("a", str); }}
test "Terminal: insert mode with space" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 2 }); defer t.deinit(alloc);
for ("hello") |c| try t.print(c); t.setCursorPos(1, 2); t.modes.set(.insert, true); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("hXello", str); }}
test "Terminal: insert mode doesn't wrap pushed characters" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 2 }); defer t.deinit(alloc);
for ("hello") |c| try t.print(c); t.setCursorPos(1, 2); t.modes.set(.insert, true); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("hXell", str); }}
test "Terminal: insert mode does nothing at the end of the line" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 2 }); defer t.deinit(alloc);
for ("hello") |c| try t.print(c); t.modes.set(.insert, true); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("hello\nX", str); }}
test "Terminal: insert mode with wide characters" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 2 }); defer t.deinit(alloc);
for ("hello") |c| try t.print(c); t.setCursorPos(1, 2); t.modes.set(.insert, true); try t.print('😀'); // 0x1F600
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("h😀el", str); }}
test "Terminal: insert mode with wide characters at end" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 2 }); defer t.deinit(alloc);
for ("well") |c| try t.print(c); t.modes.set(.insert, true); try t.print('😀'); // 0x1F600
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("well\n😀", str); }}
test "Terminal: insert mode pushing off wide character" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 2 }); defer t.deinit(alloc);
for ("123") |c| try t.print(c); try t.print('😀'); // 0x1F600 t.modes.set(.insert, true); t.setCursorPos(1, 1); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X123", str); }}
test "Terminal: deleteChars" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2);
t.clearDirty(); t.deleteChars(2); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ADE", str); }}
test "Terminal: deleteChars zero count" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2);
t.clearDirty(); t.deleteChars(0); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDE", str); }}
test "Terminal: deleteChars more than half" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2);
t.clearDirty(); t.deleteChars(3); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AE", str); }}
test "Terminal: deleteChars more than line width" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2);
t.clearDirty(); t.deleteChars(10); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A", str); }}
test "Terminal: deleteChars should shift left" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2);
t.clearDirty(); t.deleteChars(1); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ACDE", str); }}
test "Terminal: deleteChars resets pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.deleteChars(1); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDX", str); }}
test "Terminal: deleteChars resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE123") |c| try t.print(c); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; const row = list_cell.row; try testing.expect(row.wrap); } t.setCursorPos(1, 1); t.deleteChars(1);
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; const row = list_cell.row; try testing.expect(!row.wrap); }
try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("XCDE\n123", str); }}
test "Terminal: deleteChars simple operation" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.setCursorPos(1, 3);
t.clearDirty(); t.deleteChars(2); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AB23", str); }}
test "Terminal: deleteChars preserves background sgr" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 10 }); defer t.deinit(alloc);
for ("ABC123") |c| try t.print(c); t.setCursorPos(1, 3); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.deleteChars(2);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AB23", str); } for (t.cols - 2..t.cols) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 0, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); }}
test "Terminal: deleteChars outside scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 6, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.scrolling_region.left = 2; t.scrolling_region.right = 4; try testing.expect(t.screens.active.cursor.pending_wrap); t.clearDirty(); t.deleteChars(2); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.screens.active.cursor.pending_wrap);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC123", str); }}
test "Terminal: deleteChars inside scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 6, .rows = 10 }); defer t.deinit(alloc);
try t.printString("ABC123"); t.scrolling_region.left = 2; t.scrolling_region.right = 4; t.setCursorPos(1, 4);
t.clearDirty(); t.deleteChars(1); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC2 3", str); }}
test "Terminal: deleteChars split wide character from spacer tail" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 6, .rows = 10 }); defer t.deinit(alloc);
try t.printString("A橋123"); t.setCursorPos(1, 3); t.deleteChars(1);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A 123", str); }}
test "Terminal: deleteChars split wide character from wide" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 6, .rows = 10 }); defer t.deinit(alloc);
try t.printString("橋123"); t.setCursorPos(1, 1); t.deleteChars(1);
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, '1'), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: deleteChars split wide character from end" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 6, .rows = 10 }); defer t.deinit(alloc);
try t.printString("A橋123"); t.setCursorPos(1, 1); t.deleteChars(1);
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x6A4B), cell.content.codepoint); try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Terminal: deleteChars with a spacer head at the end" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 5, .rows = 10 }); defer t.deinit(alloc);
try t.printString("0123橋123"); { const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; const row = list_cell.row; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); try testing.expect(row.wrap); }
t.setCursorPos(1, 1); t.deleteChars(1);
{ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); }}
test "Terminal: deleteChars split wide character tail" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setCursorPos(1, t.cols - 1); try t.print(0x6A4B); // 橋 t.carriageReturn(); t.deleteChars(t.cols - 1); try t.print('0');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("0", str); }}
test "Terminal: deleteChars wide char boundary conditions" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 1, .cols = 8 }); defer t.deinit(alloc);
// EXPLANATION(qwerasd): // // There are 3 or 4 boundaries to be concerned with in deleteChars, // depending on how you count them. Consider the following terminal: // // +--------+ // 0 |.ABCDEF.| // : ^ : (^ = cursor) // +--------+ // // if we DCH 3 we get // // +--------+ // 0 |.DEF....| // +--------+ // // The boundaries exist at the following points then: // // +--------+ // 0 |.ABCDEF.| // :11 22 33: // +--------+ // // I'm counting 2 for double since it's both the end of the deleted // content and the start of the content that is shifted in to place. // // Now consider wide characters (represented as `WW`) at these boundaries: // // +--------+ // 0 |WWaWWbWW| // : ^ : (^ = cursor) // : ^^^ : (^ = deleted by DCH 3) // +--------+ // // -> DCH 3 // -> The first 2 wide characters are split & destroyed (verified in xterm) // // +--------+ // 0 |..bWW...| // +--------+
try t.printString("😀a😀b😀"); { const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings("😀a😀b😀", str); }
t.setCursorPos(1, 2); t.deleteChars(3); t.screens.active.cursor.page_pin.node.data.assertIntegrity();
{ const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings(" b😀", str); }}
test "Terminal: deleteChars wide char wrap boundary conditions" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 3, .cols = 8 }); defer t.deinit(alloc);
// EXPLANATION(qwerasd): // (cont. from "Terminal: deleteChars wide char boundary conditions") // // Additionally consider soft-wrapped wide chars (`H` = spacer head): // // +--------+ // 0 |.......H… // 1 …WWabcdeH… // : ^ : (^ = cursor) // : ^^^ : (^ = deleted by DCH 3) // 2 …WW......| // +--------+ // // -> DCH 3 // -> First wide character split and destroyed, including spacer head, // second spacer head removed (verified in xterm). // -> Wrap state of row reset // // +--------+ // 0 |........| // 1 |.cde....| // 2 |WW......| // +--------+ //
try t.printString(".......😀abcde😀......"); { const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings(".......\n😀abcde\n😀......", str);
const unwrapped = try t.plainStringUnwrapped(alloc); defer testing.allocator.free(unwrapped); try testing.expectEqualStrings(".......😀abcde😀......", unwrapped); }
t.setCursorPos(2, 2); t.deleteChars(3); t.screens.active.cursor.page_pin.node.data.assertIntegrity();
{ const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings(".......\n cde\n😀......", str);
const unwrapped = try t.plainStringUnwrapped(alloc); defer testing.allocator.free(unwrapped); try testing.expectEqualStrings("....... cde\n😀......", unwrapped); }}
test "Terminal: deleteChars wide char across right margin" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 3, .cols = 8 }); defer t.deinit(alloc);
// scroll region // VVVVVV // +-######-+ // |.abcdeWW| // : ^ : (^ = cursor) // +--------+ // // DCH 1
try t.printString("123456橋"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(2, 7);
{ const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings("123456橋", str); }
t.setCursorPos(1, 2); t.deleteChars(1); t.screens.active.cursor.page_pin.node.data.assertIntegrity();
// NOTE: This behavior is slightly inconsistent with xterm. xterm // _visually_ splits the wide character (half the wide character shows // up in col 6 and half in col 8). In all other wide char split scenarios, // xterm clears the cell. Therefore, we've chosen to clear the cell here. // Given we have space, we also could actually preserve it, but I haven't // yet found a terminal that behaves that way. We should be open to // revisiting this behavior but for now we're going with the simpler // impl. { const str = try t.plainString(alloc); defer testing.allocator.free(str); try testing.expectEqualStrings("13456", str); }}
test "Terminal: saveCursor" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} }); t.screens.active.charset.gr = .G3; t.modes.set(.origin, true); t.saveCursor(); t.screens.active.charset.gr = .G0; try t.setAttribute(.{ .unset = {} }); t.modes.set(.origin, false); t.restoreCursor(); try testing.expect(t.screens.active.cursor.style.flags.bold); try testing.expect(t.screens.active.charset.gr == .G3); try testing.expect(t.modes.get(.origin));}
test "Terminal: saveCursor position" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
t.setCursorPos(1, 5); try t.print('A'); t.saveCursor(); t.setCursorPos(1, 1); try t.print('B'); t.restoreCursor(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("B AX", str); }}
test "Terminal: saveCursor pending wrap state" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setCursorPos(1, 5); try t.print('A'); t.saveCursor(); t.setCursorPos(1, 1); try t.print('B'); t.restoreCursor(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("B A\nX", str); }}
test "Terminal: saveCursor origin mode" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
t.modes.set(.origin, true); t.saveCursor(); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setTopAndBottomMargin(2, 4); t.restoreCursor(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X", str); }}
test "Terminal: saveCursor resize" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
t.setCursorPos(1, 10); t.saveCursor(); try t.resize(alloc, 5, 5); t.restoreCursor(); try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); }}
test "Terminal: saveCursor protected pen" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); try testing.expect(t.screens.active.cursor.protected); t.setCursorPos(1, 10); t.saveCursor(); t.setProtectedMode(.off); try testing.expect(!t.screens.active.cursor.protected); t.restoreCursor(); try testing.expect(t.screens.active.cursor.protected);}
test "Terminal: saveCursor doesn't modify hyperlink state" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null); const id = t.screens.active.cursor.hyperlink_id; t.saveCursor(); try testing.expectEqual(id, t.screens.active.cursor.hyperlink_id); t.restoreCursor(); try testing.expectEqual(id, t.screens.active.cursor.hyperlink_id);}
test "Terminal: restoreCursor uses default style on OutOfSpace" { // Tests that restoreCursor falls back to default style when // manualStyleUpdate fails with OutOfSpace (can't split a 1-row page // and styles are at max capacity). const alloc = testing.allocator;
// Use a single row so the page can't be split var t = try init(alloc, .{ .cols = 10, .rows = 1 }); defer t.deinit(alloc);
// Set a style and save the cursor try t.setAttribute(.{ .bold = {} }); t.saveCursor();
// Clear the style try t.setAttribute(.{ .unset = {} }); try testing.expect(!t.screens.active.cursor.style.flags.bold);
// Fill the style map to max capacity const max_styles = std.math.maxInt(size.CellCountInt); while (t.screens.active.cursor.page_pin.node.data.capacity.styles < max_styles) { _ = t.screens.active.increaseCapacity( t.screens.active.cursor.page_pin.node, .styles, ) catch break; }
const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(max_styles, page.capacity.styles);
// Fill all style slots using the StyleSet's layout capacity which accounts // for the load factor. The capacity in the layout is the actual max number // of items that can be stored. { page.pauseIntegrityChecks(true); defer page.pauseIntegrityChecks(false); defer page.assertIntegrity();
const max_items = page.styles.layout.cap; var n: usize = 1; while (n < max_items) : (n += 1) { _ = page.styles.add( page.memory, .{ .bg_color = .{ .rgb = @bitCast(@as(u24, @intCast(n))) } }, ) catch break; } }
// Restore cursor - should fall back to default style since page // can't be split (1 row) and styles are at max capacity t.restoreCursor();
// The style should be reset to default because OutOfSpace occurred try testing.expect(!t.screens.active.cursor.style.flags.bold); try testing.expectEqual(style.default_id, t.screens.active.cursor.style_id);}
test "Terminal: setProtectedMode" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); defer t.deinit(alloc);
try testing.expect(!t.screens.active.cursor.protected); t.setProtectedMode(.off); try testing.expect(!t.screens.active.cursor.protected); t.setProtectedMode(.iso); try testing.expect(t.screens.active.cursor.protected); t.setProtectedMode(.dec); try testing.expect(t.screens.active.cursor.protected); t.setProtectedMode(.off); try testing.expect(!t.screens.active.cursor.protected);}
test "Terminal: eraseLine simple erase right" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 3); t.clearDirty(); t.eraseLine(.right, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AB", str); }}
test "Terminal: eraseLine resets pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.eraseLine(.right, false); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('B');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABCDB", str); }}
test "Terminal: eraseLine resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE123") |c| try t.print(c); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; try testing.expect(list_cell.row.wrap); }
t.setCursorPos(1, 1); t.eraseLine(.right, false);
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; try testing.expect(!list_cell.row.wrap); } try t.print('X');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("X\n123", str); }}
test "Terminal: eraseLine right preserves background sgr" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.eraseLine(.right, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A", str); for (1..5) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 0, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); } }}
test "Terminal: eraseLine right wide character" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
for ("AB") |c| try t.print(c); try t.print('橋'); for ("DE") |c| try t.print(c); t.setCursorPos(1, 4); t.clearDirty(); t.eraseLine(.right, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AB", str); }}
test "Terminal: eraseLine right protected attributes respected with iso" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); t.clearDirty(); t.eraseLine(.right, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC", str); }}
test "Terminal: eraseLine right protected attributes ignored with dec most recent" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(1, 2); t.clearDirty(); t.eraseLine(.right, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A", str); }}
test "Terminal: eraseLine right protected attributes ignored with dec set" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 2); t.clearDirty(); t.eraseLine(.right, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A", str); }}
test "Terminal: eraseLine right protected requested" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
for ("12345678") |c| try t.print(c); t.setCursorPos(t.screens.active.cursor.y + 1, 6); t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y + 1, 4); t.clearDirty(); t.eraseLine(.right, true); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("123 X", str); }}
test "Terminal: eraseLine simple erase left" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 3); t.clearDirty(); t.eraseLine(.left, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" DE", str); }}
test "Terminal: eraseLine left resets wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap); t.clearDirty(); t.eraseLine(.left, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(!t.screens.active.cursor.pending_wrap); try t.print('B');
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" B", str); }}
test "Terminal: eraseLine left preserves background sgr" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.eraseLine(.left, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" CDE", str); for (0..2) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 0, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); } }}
test "Terminal: eraseLine left wide character" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
for ("AB") |c| try t.print(c); try t.print('橋'); for ("DE") |c| try t.print(c); t.setCursorPos(1, 3); t.clearDirty(); t.eraseLine(.left, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" DE", str); }}
test "Terminal: eraseLine left protected attributes respected with iso" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); t.clearDirty(); t.eraseLine(.left, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC", str); }}
test "Terminal: eraseLine left protected attributes ignored with dec most recent" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(1, 2); t.clearDirty(); t.eraseLine(.left, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" C", str); }}
test "Terminal: eraseLine left protected attributes ignored with dec set" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 2); t.clearDirty(); t.eraseLine(.left, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" C", str); }}
test "Terminal: eraseLine left protected requested" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
for ("123456789") |c| try t.print(c); t.setCursorPos(t.screens.active.cursor.y + 1, 6); t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y + 1, 8); t.clearDirty(); t.eraseLine(.left, true); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X 9", str); }}
test "Terminal: eraseLine complete preserves background sgr" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.eraseLine(.complete, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); for (0..5) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 0, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); } }}
test "Terminal: eraseLine complete protected attributes respected with iso" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); t.clearDirty(); t.eraseLine(.complete, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC", str); }}
test "Terminal: eraseLine complete protected attributes ignored with dec most recent" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(1, 2); t.clearDirty(); t.eraseLine(.complete, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }}
test "Terminal: eraseLine complete protected attributes ignored with dec set" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 2); t.clearDirty(); t.eraseLine(.complete, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }}
test "Terminal: eraseLine complete protected requested" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
for ("123456789") |c| try t.print(c); t.setCursorPos(t.screens.active.cursor.y + 1, 6); t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y + 1, 8); t.clearDirty(); t.eraseLine(.complete, true); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); }}
test "Terminal: tabClear single" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 30, .rows = 5 }); defer t.deinit(alloc);
t.horizontalTab(); t.tabClear(.current); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); t.setCursorPos(1, 1); t.horizontalTab(); try testing.expectEqual(@as(usize, 16), t.screens.active.cursor.x);}
test "Terminal: tabClear all" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 30, .rows = 5 }); defer t.deinit(alloc);
t.tabClear(.all); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); t.setCursorPos(1, 1); t.horizontalTab(); try testing.expectEqual(@as(usize, 29), t.screens.active.cursor.x);}
test "Terminal: printRepeat simple" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("A"); try t.printRepeat(1); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AA", str); }}
test "Terminal: printRepeat wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString(" A"); try t.printRepeat(1); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" A\nA", str); }}
test "Terminal: printRepeat no previous character" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printRepeat(1); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }}
test "Terminal: printAttributes" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
var storage: [64]u8 = undefined;
{ try t.setAttribute(.{ .direct_color_fg = .{ .r = 1, .g = 2, .b = 3 } }); defer t.setAttribute(.unset) catch unreachable; const buf = try t.printAttributes(&storage); try testing.expectEqualStrings("0;38:2::1:2:3", buf); }
{ try t.setAttribute(.bold); try t.setAttribute(.{ .direct_color_bg = .{ .r = 1, .g = 2, .b = 3 } }); defer t.setAttribute(.unset) catch unreachable; const buf = try t.printAttributes(&storage); try testing.expectEqualStrings("0;1;48:2::1:2:3", buf); }
{ try t.setAttribute(.bold); try t.setAttribute(.faint); try t.setAttribute(.italic); try t.setAttribute(.{ .underline = .single }); try t.setAttribute(.blink); try t.setAttribute(.inverse); try t.setAttribute(.invisible); try t.setAttribute(.strikethrough); try t.setAttribute(.{ .direct_color_fg = .{ .r = 100, .g = 200, .b = 255 } }); try t.setAttribute(.{ .direct_color_bg = .{ .r = 101, .g = 102, .b = 103 } }); defer t.setAttribute(.unset) catch unreachable; const buf = try t.printAttributes(&storage); try testing.expectEqualStrings("0;1;2;3;4;5;7;8;9;38:2::100:200:255;48:2::101:102:103", buf); }
{ try t.setAttribute(.{ .underline = .single }); defer t.setAttribute(.unset) catch unreachable; const buf = try t.printAttributes(&storage); try testing.expectEqualStrings("0;4", buf); }
{ const buf = try t.printAttributes(&storage); try testing.expectEqualStrings("0", buf); }}
test "Terminal: eraseDisplay simple erase below" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2);
t.clearDirty(); t.eraseDisplay(.below, false);
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nD", str); }}
test "Terminal: eraseDisplay erase below preserves SGR bg" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2);
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.eraseDisplay(.below, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nD", str); for (1..5) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 1, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); } }}
test "Terminal: eraseDisplay below split multi-cell" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("AB橋C"); t.carriageReturn(); try t.linefeed(); try t.printString("DE橋F"); t.carriageReturn(); try t.linefeed(); try t.printString("GH橋I"); t.setCursorPos(2, 4); t.eraseDisplay(.below, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("AB橋C\nDE", str); }}
test "Terminal: eraseDisplay below protected attributes respected with iso" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2); t.eraseDisplay(.below, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nDEF\nGHI", str); }}
test "Terminal: eraseDisplay below protected attributes ignored with dec most recent" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(2, 2); t.eraseDisplay(.below, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nD", str); }}
test "Terminal: eraseDisplay below protected attributes ignored with dec set" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2); t.eraseDisplay(.below, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nD", str); }}
test "Terminal: eraseDisplay below protected attributes respected with force" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2); t.eraseDisplay(.below, true);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nDEF\nGHI", str); }}
test "Terminal: eraseDisplay simple erase above" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2);
t.clearDirty(); t.eraseDisplay(.above, false); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n F\nGHI", str); }}
test "Terminal: eraseDisplay erase above preserves SGR bg" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2);
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.eraseDisplay(.above, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n F\nGHI", str); for (0..2) |x| { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = @intCast(x), .y = 1, } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); } }}
test "Terminal: eraseDisplay above split multi-cell" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.printString("AB橋C"); t.carriageReturn(); try t.linefeed(); try t.printString("DE橋F"); t.carriageReturn(); try t.linefeed(); try t.printString("GH橋I"); t.setCursorPos(2, 3); t.eraseDisplay(.above, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n F\nGH橋I", str); }}
test "Terminal: eraseDisplay above protected attributes respected with iso" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2); t.eraseDisplay(.above, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nDEF\nGHI", str); }}
test "Terminal: eraseDisplay above protected attributes ignored with dec most recent" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(2, 2); t.eraseDisplay(.above, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n F\nGHI", str); }}
test "Terminal: eraseDisplay above protected attributes ignored with dec set" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2); t.eraseDisplay(.above, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n F\nGHI", str); }}
test "Terminal: eraseDisplay above protected attributes respected with force" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("DEF") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2); t.eraseDisplay(.above, true);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nDEF\nGHI", str); }}
test "Terminal: eraseDisplay protected complete" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
try t.print('A'); t.carriageReturn(); try t.linefeed(); for ("123456789") |c| try t.print(c); t.setCursorPos(t.screens.active.cursor.y + 1, 6); t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y + 1, 4);
t.clearDirty(); t.eraseDisplay(.complete, true); for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = @intCast(y), } }));
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n X", str); }}
test "Terminal: eraseDisplay protected below" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
try t.print('A'); t.carriageReturn(); try t.linefeed(); for ("123456789") |c| try t.print(c); t.setCursorPos(t.screens.active.cursor.y + 1, 6); t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y + 1, 4); t.eraseDisplay(.below, true);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("A\n123 X", str); }}
test "Terminal: eraseDisplay scroll complete" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
try t.print('A'); t.carriageReturn(); try t.linefeed(); t.eraseDisplay(.scroll_complete, false);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }}
test "Terminal: eraseDisplay protected above" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 3 }); defer t.deinit(alloc);
try t.print('A'); t.carriageReturn(); try t.linefeed(); for ("123456789") |c| try t.print(c); t.setCursorPos(t.screens.active.cursor.y + 1, 6); t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y + 1, 8); t.eraseDisplay(.above, true);
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("\n X 9", str); }}
test "Terminal: eraseDisplay complete preserves cursor" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Set our cursur try t.setAttribute(.{ .bold = {} }); try t.printString("AAAA"); try testing.expect(t.screens.active.cursor.style_id != style.default_id);
// Erasing the display may detect that our style is no longer in use // and prune our style, which we don't want because its still our // active cursor. t.eraseDisplay(.complete, false); try testing.expect(t.screens.active.cursor.style_id != style.default_id);}
test "Terminal: semantic prompt" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Prompt try t.semanticPrompt(.init(.fresh_line_new_prompt)); for ("hello") |c| try t.print(c); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x - 1, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expectEqual(.prompt, cell.semantic_content);
const row = list_cell.row; try testing.expectEqual(.prompt, row.semantic_prompt); }
// Start input but end it on EOL try t.semanticPrompt(.init(.end_prompt_start_input_terminate_eol)); t.carriageReturn(); try t.linefeed();
// Write some output try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); for ("world") |c| try t.print(c); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x - 1, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expectEqual(.output, cell.semantic_content);
const row = list_cell.row; try testing.expectEqual(.none, row.semantic_prompt); }}
test "Terminal: semantic prompt continuations" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Prompt try t.semanticPrompt(.init(.fresh_line_new_prompt)); for ("hello") |c| try t.print(c); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 5), t.screens.active.cursor.x); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x - 1, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expectEqual(.prompt, cell.semantic_content);
const row = list_cell.row; try testing.expectEqual(.prompt, row.semantic_prompt); }
// Start input but end it on EOL t.carriageReturn(); try t.linefeed(); try t.semanticPrompt(.{ .action = .prompt_start, .options_unvalidated = "k=c", });
// Write some output try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); for ("world") |c| try t.print(c); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x - 1, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expectEqual(.prompt, cell.semantic_content);
const row = list_cell.row; try testing.expectEqual(.prompt_continuation, row.semantic_prompt); }}
test "Terminal: index in prompt mode marks new row as prompt continuation" { // This tests the Fish shell workaround: when in prompt mode and we get // a newline, assume the new row is a prompt continuation (since Fish // doesn't emit OSC133 k=s markers for continuation lines). const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Start a prompt try t.semanticPrompt(.init(.prompt_start)); for ("hello") |c| try t.print(c);
// Verify first row is marked as prompt { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0, } }).?; try testing.expectEqual(.prompt, list_cell.row.semantic_prompt); }
// Now do a linefeed while still in prompt mode t.carriageReturn(); try t.linefeed();
// The new row should automatically be marked as prompt continuation { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 1, } }).?; try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt); }
// The cursor semantic content should still be prompt try testing.expectEqual(.prompt, t.screens.active.cursor.semantic_content);}
test "Terminal: index in input mode does not mark new row as prompt" { // Input mode should NOT trigger prompt continuation on newline // (only prompt mode does, not input mode) const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Start a prompt then switch to input try t.semanticPrompt(.init(.prompt_start)); for ("$ ") |c| try t.print(c); try t.semanticPrompt(.init(.end_prompt_start_input)); for ("echo \\") |c| try t.print(c);
// Linefeed while in input mode t.carriageReturn(); try t.linefeed();
// The new row should be marked as prompt continuation { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 1, } }).?; try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt); }
// Our cursor should still be in input try testing.expectEqual(.input, t.screens.active.cursor.semantic_content);}
test "Terminal: index in output mode does not mark new row as prompt" { // Output mode should NOT trigger prompt continuation const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Complete prompt cycle: prompt -> input -> output try t.semanticPrompt(.init(.prompt_start)); for ("$ ") |c| try t.print(c); try t.semanticPrompt(.init(.end_prompt_start_input)); for ("ls") |c| try t.print(c); try t.semanticPrompt(.init(.end_input_start_output));
// Linefeed while in output mode t.carriageReturn(); try t.linefeed();
// The new row should NOT be marked as a prompt { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 1, } }).?; try testing.expectEqual(.none, list_cell.row.semantic_prompt); }}
test "Terminal: OSC133C at x=0 on prompt row clears prompt mark" { // This tests the second Fish heuristic: when Fish emits a newline // then immediately sends OSC133C (start output) at column 0, we // should clear the prompt continuation mark we just set. const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Start a prompt try t.semanticPrompt(.init(.prompt_start)); for ("$ echo \\") |c| try t.print(c);
// Simulate Fish behavior: newline first (which marks next row as prompt) t.carriageReturn(); try t.linefeed();
// Verify the new row is marked as prompt continuation { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 1, } }).?; try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt); }
// Now Fish sends OSC133C at column 0 (cursor is still at x=0) try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try t.semanticPrompt(.init(.end_input_start_output));
// The prompt continuation should be cleared { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 1, } }).?; try testing.expectEqual(.none, list_cell.row.semantic_prompt); }}
test "Terminal: OSC133C at x>0 on prompt row does not clear prompt mark" { // If we're not at column 0, we shouldn't clear the prompt mark const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Start a prompt on a row try t.semanticPrompt(.init(.prompt_start)); for ("$ ") |c| try t.print(c);
// Move to a new line and mark it as prompt continuation manually t.carriageReturn(); try t.linefeed(); try t.semanticPrompt(.{ .action = .prompt_start, .options_unvalidated = "k=c", }); for ("> ") |c| try t.print(c);
// Verify the row is marked as prompt continuation { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 1, } }).?; try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt); }
// Now send OSC133C but cursor is NOT at column 0 try testing.expect(t.screens.active.cursor.x > 0); try t.semanticPrompt(.init(.end_input_start_output));
// The prompt continuation should NOT be cleared (we're not at x=0) { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 1, } }).?; try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt); }}
test "Terminal: multiple newlines in prompt mode marks all rows" { // Multiple newlines should each mark their row as prompt continuation const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Start a prompt try t.semanticPrompt(.init(.prompt_start)); for ("line1") |c| try t.print(c);
// Multiple newlines t.carriageReturn(); try t.linefeed(); for ("line2") |c| try t.print(c); t.carriageReturn(); try t.linefeed(); for ("line3") |c| try t.print(c);
// First row should be prompt { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0, } }).?; try testing.expectEqual(.prompt, list_cell.row.semantic_prompt); }
// Second and third rows should be prompt continuation { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 1, } }).?; try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt); } { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 2, } }).?; try testing.expectEqual(.prompt_continuation, list_cell.row.semantic_prompt); }}
test "Terminal: OSC133A click_events=1 sets click to click_events" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Verify default state is none try testing.expectEqual(.none, t.screens.active.semantic_prompt.click);
// OSC 133;A with click_events=1 try t.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = "click_events=1", });
try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .click_events = .absolute }, t.screens.active.semantic_prompt.click);}
test "Terminal: OSC133A click_events=2 sets click to click_events (relative)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// Verify default state is none try testing.expectEqual(.none, t.screens.active.semantic_prompt.click);
// OSC 133;A with click_events=2 try t.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = "click_events=2", });
try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .click_events = .relative }, t.screens.active.semantic_prompt.click);}
test "Terminal: OSC133A click_events=0 does not set click_events" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// OSC 133;A with click_events=0 try t.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = "click_events=0", });
// Should remain none since click_events=0 doesn't activate anything try testing.expectEqual(.none, t.screens.active.semantic_prompt.click);}
test "Terminal: OSC133A cl option sets click to cl value" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// OSC 133;A with cl=m (multiple) try t.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = "cl=m", });
try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .cl = .multiple }, t.screens.active.semantic_prompt.click);}
test "Terminal: OSC133A cl=line sets click to line" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
try t.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = "cl=line", });
try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .cl = .line }, t.screens.active.semantic_prompt.click);}
test "Terminal: OSC133A click_events=1 takes priority over cl" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// OSC 133;A with both click_events=1 and cl=m try t.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = "click_events=1;cl=m", });
// click_events should take priority try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .click_events = .absolute }, t.screens.active.semantic_prompt.click);}
test "Terminal: OSC133A click_events=0 falls back to cl" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// OSC 133;A with click_events=0 and cl=v try t.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = "click_events=0;cl=v", });
// Should fall back to cl since click_events is disabled try testing.expectEqual(Screen.SemanticPrompt.SemanticClick{ .cl = .conservative_vertical }, t.screens.active.semantic_prompt.click);}
test "Terminal: OSC133A no click options leaves click as none" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); defer t.deinit(alloc);
// OSC 133;A with no click-related options try t.semanticPrompt(.{ .action = .fresh_line_new_prompt, .options_unvalidated = "aid=123", });
try testing.expectEqual(.none, t.screens.active.semantic_prompt.click);}
test "Terminal: cursorIsAtPrompt" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 3 }); defer t.deinit(alloc);
try testing.expect(!t.cursorIsAtPrompt()); try t.semanticPrompt(.init(.prompt_start)); try testing.expect(t.cursorIsAtPrompt()); for ("$ ") |c| try t.print(c);
// Input is also a prompt try t.semanticPrompt(.init(.end_prompt_start_input)); try testing.expect(t.cursorIsAtPrompt()); for ("ls") |c| try t.print(c);
// But once we say we're starting output, we're not a prompt // (cursor is not at x=0, so the Fish heuristic doesn't trigger) try t.semanticPrompt(.init(.end_input_start_output)); // Still a prompt because this line has a prompt try testing.expect(t.cursorIsAtPrompt()); try t.linefeed(); try testing.expect(!t.cursorIsAtPrompt());
// Until we know we're at a prompt again try t.linefeed(); try t.semanticPrompt(.init(.prompt_start)); try testing.expect(t.cursorIsAtPrompt());}
test "Terminal: cursorIsAtPrompt alternate screen" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 2 }); defer t.deinit(alloc);
try testing.expect(!t.cursorIsAtPrompt()); try t.semanticPrompt(.init(.prompt_start)); try testing.expect(t.cursorIsAtPrompt());
// Secondary screen is never a prompt try t.switchScreenMode(.@"1049", true); try testing.expect(!t.cursorIsAtPrompt()); try t.semanticPrompt(.init(.prompt_start)); try testing.expect(!t.cursorIsAtPrompt());}
test "Terminal: fullReset with a non-empty pen" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } }); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0x7F } }); t.screens.active.cursor.semantic_content = .input; t.fullReset();
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expect(cell.style_id == 0); }
try testing.expectEqual(@as(style.Id, 0), t.screens.active.cursor.style_id); try testing.expectEqual(.output, t.screens.active.cursor.semantic_content);}
test "Terminal: fullReset hyperlink" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
try t.screens.active.startHyperlink("http://example.com", null); t.fullReset(); try testing.expectEqual(0, t.screens.active.cursor.hyperlink_id);}
test "Terminal: fullReset with a non-empty saved cursor" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } }); try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0x7F } }); t.saveCursor(); t.fullReset();
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expect(cell.style_id == 0); }
try testing.expectEqual(@as(style.Id, 0), t.screens.active.cursor.style_id);}
test "Terminal: fullReset origin mode" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
t.setCursorPos(3, 5); t.modes.set(.origin, true); t.fullReset();
// Origin mode should be reset and the cursor should be moved try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); try testing.expect(!t.modes.get(.origin));}
test "Terminal: fullReset status display" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
t.status_display = .status_line; t.fullReset(); try testing.expect(t.status_display == .main);}
// https://github.com/mitchellh/ghostty/issues/1607test "Terminal: fullReset clears alt screen kitty keyboard state" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator);
try t.switchScreenMode(.@"1049", true); t.screens.active.kitty_keyboard.push(.{ .disambiguate = true, .report_events = false, .report_alternates = true, .report_all = true, .report_associated = true, }); try t.switchScreenMode(.@"1049", false);
t.fullReset(); try testing.expect(t.screens.get(.alternate) == null);}
test "Terminal: fullReset default modes" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10, .default_modes = .{ .grapheme_cluster = true }, }); defer t.deinit(testing.allocator); try testing.expect(t.modes.get(.grapheme_cluster)); t.fullReset(); try testing.expect(t.modes.get(.grapheme_cluster));}
test "Terminal: fullReset tracked pins" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);
// Create a tracked pin const p = try t.screens.active.pages.trackPin(t.screens.active.cursor.page_pin.*); t.fullReset(); try testing.expect(t.screens.active.pages.pinIsValid(p.*));}
// https://github.com/mitchellh/ghostty/issues/272// This is also tested in depth in screen resize tests but I want to keep// this test around to ensure we don't regress at multiple layers.test "Terminal: resize less cols with wide char then print" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); defer t.deinit(alloc);
try t.print('x'); try t.print('😀'); // 0x1F600 try t.resize(alloc, 2, 3); t.setCursorPos(1, 2); try t.print('😀'); // 0x1F600}
// https://github.com/mitchellh/ghostty/issues/723// This was found via fuzzing so its highly specific.test "Terminal: resize with left and right margin set" { const alloc = testing.allocator; const cols = 70; const rows = 23; var t = try init(alloc, .{ .cols = cols, .rows = rows }); defer t.deinit(alloc);
t.modes.set(.enable_left_and_right_margin, true); try t.print('0'); t.modes.set(.enable_mode_3, true); try t.resize(alloc, cols, rows); t.setLeftAndRightMargin(2, 0); try t.printRepeat(1850); _ = t.modes.restore(.enable_mode_3); try t.resize(alloc, cols, rows);}
// https://github.com/mitchellh/ghostty/issues/1343test "Terminal: resize with wraparound off" { const alloc = testing.allocator; const cols = 4; const rows = 2; var t = try init(alloc, .{ .cols = cols, .rows = rows }); defer t.deinit(alloc);
t.modes.set(.wraparound, false); try t.print('0'); try t.print('1'); try t.print('2'); try t.print('3'); const new_cols = 2; try t.resize(alloc, new_cols, rows);
const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("01", str);}
test "Terminal: resize with wraparound on" { const alloc = testing.allocator; const cols = 4; const rows = 2; var t = try init(alloc, .{ .cols = cols, .rows = rows }); defer t.deinit(alloc);
t.modes.set(.wraparound, true); try t.print('0'); try t.print('1'); try t.print('2'); try t.print('3'); const new_cols = 2; try t.resize(alloc, new_cols, rows);
const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("01\n23", str);}
test "Terminal: resize with high unique style per cell" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 30, .rows = 30 }); defer t.deinit(alloc);
for (0..t.rows) |y| { for (0..t.cols) |x| { t.setCursorPos(y, x); try t.setAttribute(.{ .direct_color_bg = .{ .r = @intCast(x), .g = @intCast(y), .b = 0, } }); try t.print('x'); } }
try t.resize(alloc, 60, 30);}
test "Terminal: resize with high unique style per cell with wrapping" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 30, .rows = 30 }); defer t.deinit(alloc);
const cell_count: u16 = @intCast(t.rows * t.cols); for (0..cell_count) |i| { const r: u8 = @intCast(i >> 8); const g: u8 = @intCast(i & 0xFF);
try t.setAttribute(.{ .direct_color_bg = .{ .r = r, .g = g, .b = 0, } }); try t.print('x'); }
try t.resize(alloc, 60, 30);}
test "Terminal: resize with reflow and saved cursor" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 3 }); defer t.deinit(alloc); try t.printString("1A2B"); t.setCursorPos(2, 2); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint); }
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1A\n2B", str); }
t.saveCursor(); try t.resize(alloc, 5, 3); t.restoreCursor();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1A2B", str); }
// Verify our cursor is still in the same place { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint); }}
test "Terminal: resize with reflow and saved cursor pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 3 }); defer t.deinit(alloc); try t.printString("1A2B"); { const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = t.screens.active.cursor.x, .y = t.screens.active.cursor.y, } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint); }
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1A\n2B", str); }
t.saveCursor(); try t.resize(alloc, 5, 3); t.restoreCursor();
{ const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1A2B", str); }
// Pending wrap should be reset try t.print('X'); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1A2BX", str); }}
test "Terminal: DECCOLM without DEC mode 40" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.@"132_column", true); try t.deccolm(alloc, .@"132_cols"); try testing.expectEqual(@as(usize, 5), t.cols); try testing.expectEqual(@as(usize, 5), t.rows); try testing.expect(!t.modes.get(.@"132_column"));}
test "Terminal: DECCOLM unset" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.enable_mode_3, true); try t.deccolm(alloc, .@"80_cols"); try testing.expectEqual(@as(usize, 80), t.cols); try testing.expectEqual(@as(usize, 5), t.rows);}
test "Terminal: DECCOLM resets pending wrap" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c); try testing.expect(t.screens.active.cursor.pending_wrap);
t.modes.set(.enable_mode_3, true); try t.deccolm(alloc, .@"80_cols"); try testing.expectEqual(@as(usize, 80), t.cols); try testing.expectEqual(@as(usize, 5), t.rows); try testing.expect(!t.screens.active.cursor.pending_wrap);}
test "Terminal: DECCOLM preserves SGR bg" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0, } }); t.modes.set(.enable_mode_3, true); try t.deccolm(alloc, .@"80_cols");
{ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, .g = 0, .b = 0, }, list_cell.cell.content.color_rgb); }}
test "Terminal: DECCOLM resets scroll region" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
t.modes.set(.enable_left_and_right_margin, true); t.setTopAndBottomMargin(2, 3); t.setLeftAndRightMargin(3, 5);
t.modes.set(.enable_mode_3, true); try t.deccolm(alloc, .@"80_cols");
try testing.expect(t.modes.get(.enable_left_and_right_margin)); try testing.expectEqual(@as(usize, 0), t.scrolling_region.top); try testing.expectEqual(@as(usize, 4), t.scrolling_region.bottom); try testing.expectEqual(@as(usize, 0), t.scrolling_region.left); try testing.expectEqual(@as(usize, 79), t.scrolling_region.right);}
test "Terminal: mode 47 alt screen plain" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Print on primary screen try t.printString("1A");
// Go to alt screen with mode 47 try t.switchScreenMode(.@"47", true); try testing.expectEqual(.alternate, t.screens.active_key);
// Screen should be empty { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }
// Print on alt screen. This should be off center because // we copy the cursor over from the primary screen try t.printString("2B"); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" 2B", str); }
// Go back to primary try t.switchScreenMode(.@"47", false); try testing.expectEqual(.primary, t.screens.active_key);
// Primary screen should still have the original content { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1A", str); }
// Go back to alt screen with mode 47 try t.switchScreenMode(.@"47", true); try testing.expectEqual(.alternate, t.screens.active_key);
// Screen should retain content { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" 2B", str); }}
test "Terminal: mode 47 copies cursor both directions" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Color our cursor red try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } });
// Go to alt screen with mode 47 try t.switchScreenMode(.@"47", true); try testing.expectEqual(.alternate, t.screens.active_key);
// Verify that our style is set { try testing.expect(t.screens.active.cursor.style_id != style.default_id); const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); try testing.expect(page.styles.refCount(page.memory, t.screens.active.cursor.style_id) > 0); }
// Set a new style try t.setAttribute(.{ .direct_color_fg = .{ .r = 0, .g = 0xFF, .b = 0 } });
// Go back to primary try t.switchScreenMode(.@"47", false); try testing.expectEqual(.primary, t.screens.active_key);
// Verify that our style is still set { try testing.expect(t.screens.active.cursor.style_id != style.default_id); const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); try testing.expect(page.styles.refCount(page.memory, t.screens.active.cursor.style_id) > 0); }}
test "Terminal: mode 1047 alt screen plain" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Print on primary screen try t.printString("1A");
// Go to alt screen with mode 47 try t.switchScreenMode(.@"1047", true); try testing.expectEqual(.alternate, t.screens.active_key);
// Screen should be empty { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }
// Print on alt screen. This should be off center because // we copy the cursor over from the primary screen try t.printString("2B"); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" 2B", str); }
// Go back to primary try t.switchScreenMode(.@"1047", false); try testing.expectEqual(.primary, t.screens.active_key);
// Primary screen should still have the original content { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1A", str); }
// Go back to alt screen with mode 1047 try t.switchScreenMode(.@"1047", true); try testing.expectEqual(.alternate, t.screens.active_key);
// Screen should be empty { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }}
test "Terminal: mode 1047 copies cursor both directions" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Color our cursor red try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } });
// Go to alt screen with mode 47 try t.switchScreenMode(.@"1047", true); try testing.expectEqual(.alternate, t.screens.active_key);
// Verify that our style is set { try testing.expect(t.screens.active.cursor.style_id != style.default_id); const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); try testing.expect(page.styles.refCount(page.memory, t.screens.active.cursor.style_id) > 0); }
// Set a new style try t.setAttribute(.{ .direct_color_fg = .{ .r = 0, .g = 0xFF, .b = 0 } });
// Go back to primary try t.switchScreenMode(.@"1047", false); try testing.expectEqual(.primary, t.screens.active_key);
// Verify that our style is still set { try testing.expect(t.screens.active.cursor.style_id != style.default_id); const page = &t.screens.active.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count()); try testing.expect(page.styles.refCount(page.memory, t.screens.active.cursor.style_id) > 0); }}
test "Terminal: mode 1049 alt screen plain" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc);
// Print on primary screen try t.printString("1A");
// Go to alt screen with mode 47 try t.switchScreenMode(.@"1049", true); try testing.expectEqual(.alternate, t.screens.active_key);
// Screen should be empty { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }
// Print on alt screen. This should be off center because // we copy the cursor over from the primary screen try t.printString("2B"); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings(" 2B", str); }
// Go back to primary try t.switchScreenMode(.@"1049", false); try testing.expectEqual(.primary, t.screens.active_key);
// Primary screen should still have the original content { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1A", str); }
// Write, our cursor should be restored back. try t.printString("C"); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("1AC", str); }
// Go back to alt screen with mode 1049 try t.switchScreenMode(.@"1049", true); try testing.expectEqual(.alternate, t.screens.active_key);
// Screen should be empty { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); try testing.expectEqualStrings("", str); }}
// Reproduces a crash found by AFL++ fuzzer (afl-out/stream/default/crashes/// id:000007,sig:06,src:004522). The crash is a page integrity violation// "spacer tail not following wide" triggered during scrollUp -> deleteLines// -> clearCells. When deleteLines count >= scroll region height, all rows// are cleared (no shifting), so rowWillBeShifted is never called and wide// characters straddling the right margin boundary leave orphaned spacer_tails.test "Terminal: deleteLines wide char at right margin with full clear" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 24 }); defer t.deinit(alloc);
// Place a wide character at col 39 (1-indexed) on several rows. // The wide cell lands at col 38 (0-indexed) with spacer_tail at col 39. t.setCursorPos(10, 39); try t.print(0x4E2D); // '中'
// Set left/right scroll margins so scrolling_region.right = 38. // clearCells will clear cells[4..39], which includes the wide cell // at col 38 but NOT the spacer_tail at col 39. t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(5, 39);
// scrollUp with count >= region height causes deleteLines to clear // ALL rows without any shifting, so rowWillBeShifted is never called // and the orphaned spacer_tail at col 39 triggers a page integrity // violation in clearCells. try t.scrollUp(t.rows);}
test "Terminal: glyph APC stores session glossary entries" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 24 }); defer t.deinit(alloc);
var register_parser = glyph.CommandParser.init(alloc, 1024 * 1024); defer register_parser.deinit(); for ("r;cp=e0a0;AAAAAAAAAAAAAA==") |byte| try register_parser.feed(byte); var register_req = try register_parser.complete(alloc); defer register_req.deinit(alloc);
try testing.expectEqual(glyph.Response{ .register = .{ .cp = 0xE0A0 }, }, t.glyphProtocol(alloc, ®ister_req).?); try testing.expect(t.glyph_glossary.contains(0xE0A0)); try testing.expect(t.flags.dirty.glyph_glossary);
var query_parser = glyph.CommandParser.init(alloc, 1024 * 1024); defer query_parser.deinit(); for ("q;cp=e0a0") |byte| try query_parser.feed(byte); var query_req = try query_parser.complete(alloc); defer query_req.deinit(alloc);
try testing.expectEqual(glyph.Response{ .query = .{ .cp = 0xE0A0, .status = .{ .glossary = true }, } }, t.glyphProtocol(alloc, &query_req).?);
t.fullReset(); try testing.expect(!t.glyph_glossary.contains(0xE0A0));}