This repository has no description
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092const Screen = @This();
const std = @import("std");const build_config = @import("../build_config.zig");const Allocator = std.mem.Allocator;const assert = std.debug.assert;const ansi = @import("ansi.zig");const charsets = @import("charsets.zig");const fastmem = @import("../fastmem.zig");const kitty = @import("kitty.zig");const sgr = @import("sgr.zig");const unicode = @import("../unicode/main.zig");const Selection = @import("Selection.zig");const PageList = @import("PageList.zig");const StringMap = @import("StringMap.zig");const pagepkg = @import("page.zig");const point = @import("point.zig");const size = @import("size.zig");const style = @import("style.zig");const hyperlink = @import("hyperlink.zig");const Offset = size.Offset;const Page = pagepkg.Page;const Row = pagepkg.Row;const Cell = pagepkg.Cell;const Pin = PageList.Pin;
const log = std.log.scoped(.screen);
/// The general purpose allocator to use for all memory allocations./// Unfortunately some screen operations do require allocation.alloc: Allocator,
/// The list of pages in the screen.pages: PageList,
/// Special-case where we want no scrollback whatsoever. We have to flag/// this because max_size 0 in PageList gets rounded up to two pages so/// we can always have an active screen.no_scrollback: bool = false,
/// The current cursor positioncursor: Cursor,
/// The saved cursorsaved_cursor: ?SavedCursor = null,
/// The selection for this screen (if any). This MUST be a tracked selection/// otherwise the selection will become invalid. Instead of accessing this/// directly to set it, use the `select` function which will assert and/// automatically setup tracking.selection: ?Selection = null,
/// The charset statecharset: CharsetState = .{},
/// The current or most recent protected mode. Once a protection mode is/// set, this will never become "off" again until the screen is reset./// The current state of whether protection attributes should be set is/// set on the Cell pen; this is only used to determine the most recent/// protection mode since some sequences such as ECH depend on this.protected_mode: ansi.ProtectedMode = .off,
/// The kitty keyboard settings.kitty_keyboard: kitty.KeyFlagStack = .{},
/// Kitty graphics protocol state.kitty_images: kitty.graphics.ImageStorage = .{},
/// Dirty flags for the renderer.dirty: Dirty = .{},
/// See Terminal.Dirty. This behaves the same way.pub const Dirty = packed struct { /// Set when the selection is set or unset, regardless of if the /// selection is changed or not. selection: bool = false,
/// When an OSC8 hyperlink is hovered, we set the full screen as dirty /// because links can span multiple lines. hyperlink_hover: bool = false,};
/// The cursor position and style.pub const Cursor = struct { // The x/y position within the viewport. x: size.CellCountInt = 0, y: size.CellCountInt = 0,
/// The visual style of the cursor. This defaults to block because /// it has to default to something, but users of this struct are /// encouraged to set their own default. cursor_style: CursorStyle = .block,
/// The "last column flag (LCF)" as its called. If this is set then the /// next character print will force a soft-wrap. pending_wrap: bool = false,
/// The protected mode state of the cursor. If this is true then /// all new characters printed will have the protected state set. protected: bool = false,
/// The currently active style. This is the concrete style value /// that should be kept up to date. The style ID to use for cell writing /// is below. style: style.Style = .{},
/// The currently active style ID. The style is page-specific so when /// we change pages we need to ensure that we update that page with /// our style when used. style_id: style.Id = style.default_id,
/// The hyperlink ID that is currently active for the cursor. A value /// of zero means no hyperlink is active. (Implements OSC8, saying that /// so code search can find it.). hyperlink_id: hyperlink.Id = 0,
/// This is the implicit ID to use for hyperlinks that don't specify /// an ID. We do an overflowing add to this so repeats can technically /// happen with carefully crafted inputs but for real workloads its /// highly unlikely -- and the fix is for the TUI program to use explicit /// IDs. hyperlink_implicit_id: size.OffsetInt = 0,
/// Heap-allocated hyperlink state so that we can recreate it when /// the cursor page pin changes. We can't get it from the old screen /// state because the page may be cleared. This is heap allocated /// because its most likely null. hyperlink: ?*hyperlink.Hyperlink = null,
/// The pointers into the page list where the cursor is currently /// located. This makes it faster to move the cursor. page_pin: *PageList.Pin, page_row: *pagepkg.Row, page_cell: *pagepkg.Cell,
pub fn deinit(self: *Cursor, alloc: Allocator) void { if (self.hyperlink) |link| { link.deinit(alloc); alloc.destroy(link); } }};
/// The visual style of the cursor. Whether or not it blinks/// is determined by mode 12 (modes.zig). This mode is synchronized/// with CSI q, the same as xterm.pub const CursorStyle = enum { bar, // DECSCUSR 5, 6 block, // DECSCUSR 1, 2 underline, // DECSCUSR 3, 4
/// The cursor styles below aren't known by DESCUSR and are custom /// implemented in Ghostty. They are reported as some standard style /// if requested, though. /// Hollow block cursor. This is a block cursor with the center empty. /// Reported as DECSCUSR 1 or 2 (block). block_hollow,};
/// Saved cursor state.pub const SavedCursor = struct { x: size.CellCountInt, y: size.CellCountInt, style: style.Style, protected: bool, pending_wrap: bool, origin: bool, charset: CharsetState,};
/// State required for all charset operations.pub const CharsetState = struct { /// The list of graphical charsets by slot charsets: CharsetArray = .initFill(charsets.Charset.utf8),
/// GL is the slot to use when using a 7-bit printable char (up to 127) /// GR used for 8-bit printable chars. gl: charsets.Slots = .G0, gr: charsets.Slots = .G2,
/// Single shift where a slot is used for exactly one char. single_shift: ?charsets.Slots = null,
/// An array to map a charset slot to a lookup table. const CharsetArray = std.EnumArray(charsets.Slots, charsets.Charset);};
/// Initialize a new screen.////// max_scrollback is the amount of scrollback to keep in bytes. This/// will be rounded UP to the nearest page size because our minimum allocation/// size is that anyways.////// If max scrollback is 0, then no scrollback is kept at all.pub fn init( alloc: Allocator, cols: size.CellCountInt, rows: size.CellCountInt, max_scrollback: usize,) !Screen { // Initialize our backing pages. var pages = try PageList.init(alloc, cols, rows, max_scrollback); errdefer pages.deinit();
// Create our tracked pin for the cursor. const page_pin = try pages.trackPin(.{ .node = pages.pages.first.? }); errdefer pages.untrackPin(page_pin); const page_rac = page_pin.rowAndCell();
return .{ .alloc = alloc, .pages = pages, .no_scrollback = max_scrollback == 0, .cursor = .{ .x = 0, .y = 0, .page_pin = page_pin, .page_row = page_rac.row, .page_cell = page_rac.cell, }, };}
pub fn deinit(self: *Screen) void { self.kitty_images.deinit(self.alloc, self); self.cursor.deinit(self.alloc); self.pages.deinit();}
/// Assert that the screen is in a consistent state. This doesn't check/// all pages in the page list because that is SO SLOW even just for/// tests. This only asserts the screen specific data so callers should/// ensure they're also calling page integrity checks if necessary.pub fn assertIntegrity(self: *const Screen) void { if (build_config.slow_runtime_safety) { assert(self.cursor.x < self.pages.cols); assert(self.cursor.y < self.pages.rows);
// Our cursor x/y should always match the pin. If this doesn't // match then it indicates that the tracked pin moved and we didn't // account for it by either calling cursorReload or manually // adjusting. const pt: point.Point = self.pages.pointFromPin( .active, self.cursor.page_pin.*, ) orelse unreachable; assert(self.cursor.x == pt.active.x); assert(self.cursor.y == pt.active.y); }}
/// Reset the screen according to the logic of a DEC RIS sequence.////// - Clears the screen and attempts to reclaim memory./// - Moves the cursor to the top-left./// - Clears any cursor state: style, hyperlink, etc./// - Resets the charset/// - Clears the selection/// - Deletes all Kitty graphics/// - Resets Kitty Keyboard settings/// - Disables protection mode///pub fn reset(self: *Screen) void { // Reset our pages self.pages.reset();
// The above reset preserves tracked pins so we can still use // our cursor pin, which should be at the top-left already. const cursor_pin: *PageList.Pin = self.cursor.page_pin; assert(cursor_pin.node == self.pages.pages.first.?); assert(cursor_pin.x == 0); assert(cursor_pin.y == 0); const cursor_rac = cursor_pin.rowAndCell(); self.cursor.deinit(self.alloc); self.cursor = .{ .page_pin = cursor_pin, .page_row = cursor_rac.row, .page_cell = cursor_rac.cell, };
// Reset kitty graphics storage self.kitty_images.deinit(self.alloc, self); self.kitty_images = .{ .dirty = true };
// Reset our basic state self.saved_cursor = null; self.charset = .{}; self.kitty_keyboard = .{}; self.protected_mode = .off; self.clearSelection();}
/// Clone the screen.////// This will copy:////// - Screen dimensions/// - Screen data (cell state, etc.) for the region////// Anything not mentioned above is NOT copied. Some of this is for/// very good reason:////// - Kitty images have a LOT of data. This is not efficient to copy./// Use a lock and access the image data. The dirty bit is there for/// a reason./// - Cursor location can be expensive to calculate with respect to the/// specified region. It is faster to grab the cursor from the old/// screen and then move it to the new screen./// - Current hyperlink cursor state has heap allocations. Since clone/// is only for read-only operations, it is better to not have any/// hyperlink state. Note that already-written hyperlinks are cloned.////// If not mentioned above, then there isn't a specific reason right now/// to not copy some data other than we probably didn't need it and it/// isn't necessary for screen coherency.////// Other notes:////// - The viewport will always be set to the active area of the new/// screen. This is the bottom "rows" rows./// - If the clone region is smaller than a viewport area, blanks will/// be filled in at the bottom.///pub fn clone( self: *const Screen, alloc: Allocator, top: point.Point, bot: ?point.Point,) !Screen { return try self.clonePool(alloc, null, top, bot);}
/// Same as clone but you can specify a custom memory pool to use for/// the screen.pub fn clonePool( self: *const Screen, alloc: Allocator, pool: ?*PageList.MemoryPool, top: point.Point, bot: ?point.Point,) !Screen { // Create a tracked pin remapper for our selection and cursor. Note // that we may want to expose this generally in the future but at the // time of doing this we don't need to. var pin_remap = PageList.Clone.TrackedPinsRemap.init(alloc); defer pin_remap.deinit();
var pages = try self.pages.clone(.{ .top = top, .bot = bot, .memory = if (pool) |p| .{ .pool = p, } else .{ .alloc = alloc, }, .tracked_pins = &pin_remap, }); errdefer pages.deinit();
// Find our cursor. If the cursor isn't in the cloned area, we move it // to the top-left arbitrarily because a screen must have SOME cursor. const cursor: Cursor = cursor: { if (pin_remap.get(self.cursor.page_pin)) |p| remap: { const page_rac = p.rowAndCell(); const pt = pages.pointFromPin(.active, p.*) orelse break :remap; break :cursor .{ .x = @intCast(pt.active.x), .y = @intCast(pt.active.y), .page_pin = p, .page_row = page_rac.row, .page_cell = page_rac.cell, }; }
const page_pin = try pages.trackPin(.{ .node = pages.pages.first.? }); const page_rac = page_pin.rowAndCell(); break :cursor .{ .x = 0, .y = 0, .page_pin = page_pin, .page_row = page_rac.row, .page_cell = page_rac.cell, }; };
// Preserve our selection if we have one. const sel: ?Selection = if (self.selection) |sel| sel: { assert(sel.tracked());
const ordered: struct { tl: *Pin, br: *Pin, } = switch (sel.order(self)) { .forward, .mirrored_forward => .{ .tl = sel.bounds.tracked.start, .br = sel.bounds.tracked.end, }, .reverse, .mirrored_reverse => .{ .tl = sel.bounds.tracked.end, .br = sel.bounds.tracked.start, }, };
const start_pin = pin_remap.get(ordered.tl) orelse start: { // No start means it is outside the cloned area.
// If we have no end pin then either // (1) our whole selection is outside the cloned area or // (2) our cloned area is within the selection if (pin_remap.get(ordered.br) == null) { // We check if the selection bottom right pin is above // the cloned area or if the top left pin is below the // cloned area, in either of these cases it means that // the selection is fully out of bounds, so we have no // selection in the cloned area and break out now. const clone_top = self.pages.pin(top) orelse break :sel null; const clone_top_y = self.pages.pointFromPin( .screen, clone_top, ).?.screen.y; if (self.pages.pointFromPin( .screen, ordered.br.*, ).?.screen.y < clone_top_y) break :sel null; if (self.pages.pointFromPin( .screen, ordered.tl.*, ).?.screen.y > clone_top_y) break :sel null; }
// We move the top pin back in bounds to the top row. break :start try pages.trackPin(.{ .node = pages.pages.first.?, .x = if (sel.rectangle) ordered.tl.x else 0, }); };
// If we got to this point it means that the selection is not // fully out of bounds, so we move the bottom right pin back // in bounds if it isn't already. const end_pin = pin_remap.get(ordered.br) orelse try pages.trackPin(.{ .node = pages.pages.last.?, .x = if (sel.rectangle) ordered.br.x else pages.cols - 1, .y = pages.pages.last.?.data.size.rows - 1, });
break :sel .{ .bounds = .{ .tracked = .{ .start = start_pin, .end = end_pin, } }, .rectangle = sel.rectangle, }; } else null;
const result: Screen = .{ .alloc = alloc, .pages = pages, .no_scrollback = self.no_scrollback, .cursor = cursor, .selection = sel, .dirty = self.dirty, }; result.assertIntegrity(); return result;}
/// Adjust the capacity of a page within the pagelist of this screen./// This handles some accounting if the page being modified is the/// cursor page.pub fn adjustCapacity( self: *Screen, node: *PageList.List.Node, adjustment: PageList.AdjustCapacity,) PageList.AdjustCapacityError!*PageList.List.Node { // If the page being modified isn't our cursor page then // this is a quick operation because we have no additional // accounting. if (node != self.cursor.page_pin.node) { return try self.pages.adjustCapacity(node, adjustment); }
// We're modifying the cursor page. When we adjust the // capacity below it will be short the ref count on our // current style and hyperlink, so we need to init those. const new_node = try self.pages.adjustCapacity(node, adjustment); const new_page: *Page = &new_node.data;
// Re-add the style, if the page somehow doesn't have enough // memory to add it, we emit a warning and gracefully degrade // to the default style for the cursor. if (self.cursor.style_id != 0) { self.cursor.style_id = new_page.styles.add( new_page.memory, self.cursor.style, ) catch |err| id: { // TODO: Should we increase the capacity further in this case? log.warn( "(Screen.adjustCapacity) Failed to add cursor style back to page, err={}", .{err}, );
// Reset the cursor style. self.cursor.style = .{}; break :id style.default_id; }; }
// Re-add the hyperlink, if the page somehow doesn't have enough // memory to add it, we emit a warning and gracefully degrade to // no hyperlink. if (self.cursor.hyperlink) |link| { // So we don't attempt to free any memory in the replaced page. self.cursor.hyperlink_id = 0; self.cursor.hyperlink = null;
// Re-add self.startHyperlinkOnce(link.*) catch |err| { // TODO: Should we increase the capacity further in this case? log.warn( "(Screen.adjustCapacity) Failed to add cursor hyperlink back to page, err={}", .{err}, ); };
// Remove our old link link.deinit(self.alloc); self.alloc.destroy(link); }
// Reload the cursor information because the pin changed. // So our page row/cell and so on are all off. self.cursorReload();
return new_node;}
pub fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { assert(self.cursor.x + n < self.pages.cols); const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); return @ptrCast(cell + n);}
pub fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { assert(self.cursor.x >= n); const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); return @ptrCast(cell - n);}
pub fn cursorCellEndOfPrev(self: *Screen) *pagepkg.Cell { assert(self.cursor.y > 0);
var page_pin = self.cursor.page_pin.up(1).?; page_pin.x = self.pages.cols - 1; const page_rac = page_pin.rowAndCell(); return page_rac.cell;}
/// Move the cursor right. This is a specialized function that is very fast/// if the caller can guarantee we have space to move right (no wrapping).pub fn cursorRight(self: *Screen, n: size.CellCountInt) void { assert(self.cursor.x + n < self.pages.cols); defer self.assertIntegrity();
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); self.cursor.page_cell = @ptrCast(cell + n); self.cursor.page_pin.x += n; self.cursor.x += n;}
/// Move the cursor left.pub fn cursorLeft(self: *Screen, n: size.CellCountInt) void { assert(self.cursor.x >= n); defer self.assertIntegrity();
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); self.cursor.page_cell = @ptrCast(cell - n); self.cursor.page_pin.x -= n; self.cursor.x -= n;}
/// Move the cursor up.////// Precondition: The cursor is not at the top of the screen.pub fn cursorUp(self: *Screen, n: size.CellCountInt) void { assert(self.cursor.y >= n); defer self.assertIntegrity();
self.cursor.y -= n; // Must be set before cursorChangePin const page_pin = self.cursor.page_pin.up(n).?; self.cursorChangePin(page_pin); const page_rac = page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell;}
pub fn cursorRowUp(self: *Screen, n: size.CellCountInt) *pagepkg.Row { assert(self.cursor.y >= n); defer self.assertIntegrity();
const page_pin = self.cursor.page_pin.up(n).?; const page_rac = page_pin.rowAndCell(); return page_rac.row;}
/// Move the cursor down.////// Precondition: The cursor is not at the bottom of the screen.pub fn cursorDown(self: *Screen, n: size.CellCountInt) void { assert(self.cursor.y + n < self.pages.rows); defer self.assertIntegrity();
self.cursor.y += n; // Must be set before cursorChangePin
// We move the offset into our page list to the next row and then // get the pointers to the row/cell and set all the cursor state up. const page_pin = self.cursor.page_pin.down(n).?; self.cursorChangePin(page_pin); const page_rac = page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell;}
/// Move the cursor to some absolute horizontal position.pub fn cursorHorizontalAbsolute(self: *Screen, x: size.CellCountInt) void { assert(x < self.pages.cols); defer self.assertIntegrity();
self.cursor.page_pin.x = x; const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_cell = page_rac.cell; self.cursor.x = x;}
/// Move the cursor to some absolute position.pub fn cursorAbsolute(self: *Screen, x: size.CellCountInt, y: size.CellCountInt) void { assert(x < self.pages.cols); assert(y < self.pages.rows); defer self.assertIntegrity();
var page_pin = if (y < self.cursor.y) self.cursor.page_pin.up(self.cursor.y - y).? else if (y > self.cursor.y) self.cursor.page_pin.down(y - self.cursor.y).? else self.cursor.page_pin.*; page_pin.x = x; self.cursor.x = x; // Must be set before cursorChangePin self.cursor.y = y; self.cursorChangePin(page_pin); const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell;}
/// Reloads the cursor pointer information into the screen. This is expensive/// so it should only be done in cases where the pointers are invalidated/// in such a way that its difficult to recover otherwise.pub fn cursorReload(self: *Screen) void { defer self.assertIntegrity();
// Our tracked pin is ALWAYS accurate, so we derive the active // point from the pin. If this returns null it means our pin // points outside the active area. In that case, we update the // pin to be the top-left. const pt: point.Point = self.pages.pointFromPin( .active, self.cursor.page_pin.*, ) orelse reset: { const pin = self.pages.pin(.{ .active = .{} }).?; self.cursor.page_pin.* = pin; break :reset self.pages.pointFromPin(.active, pin).?; };
self.cursor.x = @intCast(pt.active.x); self.cursor.y = @intCast(pt.active.y); const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell;
// If we have a style, we need to ensure it is in the page because this // method may also be called after a page change. if (self.cursor.style_id != style.default_id) { self.manualStyleUpdate() catch |err| { // This failure should not happen because manualStyleUpdate // handles page splitting, overflow, and more. This should only // happen if we're out of RAM. In this case, we'll just degrade // gracefully back to the default style. log.err("failed to update style on cursor reload err={}", .{err}); self.cursor.style = .{}; self.cursor.style_id = 0; }; }}
/// Scroll the active area and keep the cursor at the bottom of the screen./// This is a very specialized function but it keeps it fast.pub fn cursorDownScroll(self: *Screen) !void { assert(self.cursor.y == self.pages.rows - 1); defer self.assertIntegrity();
// Scrolling dirties the images because it updates their placements pins. self.kitty_images.dirty = true;
// If we have no scrollback, then we shift all our rows instead. if (self.no_scrollback) { // If we have a single-row screen, we have no rows to shift // so our cursor is in the correct place we just have to clear // the cells. if (self.pages.rows == 1) { const page: *Page = &self.cursor.page_pin.node.data; self.clearCells( page, self.cursor.page_row, page.getCells(self.cursor.page_row), );
var dirty = page.dirtyBitSet(); dirty.set(0); } else { // eraseRow will shift everything below it up. try self.pages.eraseRow(.{ .active = .{} });
// Note we don't need to mark anything dirty in this branch // because eraseRow will mark all the rotated rows as dirty // in the entire page.
// We need to move our cursor down one because eraseRows will // preserve our pin directly and we're erasing one row. const page_pin = self.cursor.page_pin.down(1).?; self.cursorChangePin(page_pin); const page_rac = page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell;
// The above may clear our cursor so we need to update that // again. If this fails (highly unlikely) we just reset // the cursor. self.manualStyleUpdate() catch |err| { // This failure should not happen because manualStyleUpdate // handles page splitting, overflow, and more. This should only // happen if we're out of RAM. In this case, we'll just degrade // gracefully back to the default style. log.err("failed to update style on cursor scroll err={}", .{err}); self.cursor.style = .{}; self.cursor.style_id = 0; }; } } else { const old_pin = self.cursor.page_pin.*;
// Grow our pages by one row. The PageList will handle if we need to // allocate, prune scrollback, whatever. _ = try self.pages.grow();
// If our pin page change it means that the page that the pin // was on was pruned. In this case, grow() moves the pin to // the top-left of the new page. This effectively moves it by // one already, we just need to fix up the x value. const page_pin = if (old_pin.node == self.cursor.page_pin.node) self.cursor.page_pin.down(1).? else reuse: { var pin = self.cursor.page_pin.*; pin.x = self.cursor.x; break :reuse pin; };
// These assertions help catch some pagelist math errors. Our // x/y should be unchanged after the grow. if (build_config.slow_runtime_safety) { const active = self.pages.pointFromPin( .active, page_pin, ).?.active; assert(active.x == self.cursor.x); assert(active.y == self.cursor.y); }
self.cursorChangePin(page_pin); const page_rac = page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell;
// Our new row is always dirty self.cursorMarkDirty();
// Clear the new row so it gets our bg color. We only do this // if we have a bg color at all. if (self.cursor.style.bg_color != .none) { const page: *Page = &page_pin.node.data; self.clearCells( page, self.cursor.page_row, page.getCells(self.cursor.page_row), ); } }
if (self.cursor.style_id != style.default_id) { // The newly created line needs to be styled according to // the bg color if it is set. if (self.cursor.style.bgCell()) |blank_cell| { const cell_current: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); const cells = cell_current - self.cursor.x; @memset(cells[0..self.pages.cols], blank_cell); } }}
/// This scrolls the active area at and above the cursor./// The lines below the cursor are not scrolled.pub fn cursorScrollAbove(self: *Screen) !void { // We unconditionally mark the cursor row as dirty here because // the cursor always changes page rows inside this function, and // when that happens it can mean the text in the old row needs to // be re-shaped because the cursor splits runs to break ligatures. self.cursor.page_pin.markDirty();
// If the cursor is on the bottom of the screen, its faster to use // our specialized function for that case. if (self.cursor.y == self.pages.rows - 1) { return try self.cursorDownScroll(); }
defer self.assertIntegrity();
// Logic below assumes we always have at least one row that isn't moving assert(self.cursor.y < self.pages.rows - 1);
// Explanation: // We don't actually move everything that's at or above the cursor row, // since this would require us to shift up our ENTIRE scrollback, which // would be ridiculously expensive. Instead, we insert a new row at the // end of the pagelist (`grow()`), and move everything BELOW the cursor // DOWN by one row. This has the same practical result but it's a whole // lot cheaper in 99% of cases.
const old_pin = self.cursor.page_pin.*; if (try self.pages.grow()) |_| { try self.cursorScrollAboveRotate(); } else { // In this case, it means grow() didn't allocate a new page.
if (self.cursor.page_pin.node == self.pages.pages.last) { // If we're on the last page we can do a very fast path because // all the rows we need to move around are within a single page.
// Note: we don't need to call cursorChangePin here because // the pin page is the same so there is no accounting to do // for styles or any of that. assert(old_pin.node == self.cursor.page_pin.node); self.cursor.page_pin.* = self.cursor.page_pin.down(1).?;
const pin = self.cursor.page_pin; const page: *Page = &self.cursor.page_pin.node.data;
// Rotate the rows so that the newly created empty row is at the // beginning. e.g. [ 0 1 2 3 ] in to [ 3 0 1 2 ]. var rows = page.rows.ptr(page.memory.ptr); fastmem.rotateOnceR(Row, rows[pin.y..page.size.rows]);
// Mark all our rotated rows as dirty. var dirty = page.dirtyBitSet(); dirty.setRangeValue(.{ .start = pin.y, .end = page.size.rows }, true);
// Setup our cursor caches after the rotation so it points to the // correct data const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; } else { // We didn't grow pages but our cursor isn't on the last page. // In this case we need to do more work because we need to copy // elements between pages. // // An example scenario of this is shown below: // // +----------+ = PAGE 0 // ... : : // +-------------+ ACTIVE // 4302 |1A00000000| | 0 // 4303 |2B00000000| | 1 // :^ : : = PIN 0 // 4304 |3C00000000| | 2 // +----------+ : // +----------+ : = PAGE 1 // 0 |4D00000000| | 3 // 1 |5E00000000| | 4 // +----------+ : // +-------------+ try self.cursorScrollAboveRotate(); } }
if (self.cursor.style_id != style.default_id) { // The newly created line needs to be styled according to // the bg color if it is set. if (self.cursor.style.bgCell()) |blank_cell| { const cell_current: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); const cells = cell_current - self.cursor.x; @memset(cells[0..self.pages.cols], blank_cell); } }}
fn cursorScrollAboveRotate(self: *Screen) !void { self.cursorChangePin(self.cursor.page_pin.down(1).?);
// Go through each of the pages following our pin, shift all rows // down by one, and copy the last row of the previous page. var current = self.pages.pages.last.?; while (current != self.cursor.page_pin.node) : (current = current.prev.?) { const prev = current.prev.?; const prev_page = &prev.data; const cur_page = ¤t.data; const prev_rows = prev_page.rows.ptr(prev_page.memory.ptr); const cur_rows = cur_page.rows.ptr(cur_page.memory.ptr);
// Rotate the pages down: [ 0 1 2 3 ] => [ 3 0 1 2 ] fastmem.rotateOnceR(Row, cur_rows[0..cur_page.size.rows]);
// Copy the last row of the previous page to the top of current. try cur_page.cloneRowFrom( prev_page, &cur_rows[0], &prev_rows[prev_page.size.rows - 1], );
// All rows we rotated are dirty var dirty = cur_page.dirtyBitSet(); dirty.setRangeValue(.{ .start = 0, .end = cur_page.size.rows }, true); }
// Our current is our cursor page, we need to rotate down from // our cursor and clear our row. assert(current == self.cursor.page_pin.node); const cur_page = ¤t.data; const cur_rows = cur_page.rows.ptr(cur_page.memory.ptr); fastmem.rotateOnceR(Row, cur_rows[self.cursor.page_pin.y..cur_page.size.rows]); self.clearCells( cur_page, &cur_rows[self.cursor.page_pin.y], cur_page.getCells(&cur_rows[self.cursor.page_pin.y]), );
// Set all the rows we rotated and cleared dirty var dirty = cur_page.dirtyBitSet(); dirty.setRangeValue( .{ .start = self.cursor.page_pin.y, .end = cur_page.size.rows }, true, );
// Setup cursor cache data after all the rotations so our // row is valid. const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell;}
/// Move the cursor down if we're not at the bottom of the screen. Otherwise/// scroll. Currently only used for testing.fn cursorDownOrScroll(self: *Screen) !void { if (self.cursor.y + 1 < self.pages.rows) { self.cursorDown(1); } else { try self.cursorDownScroll(); }}
/// Copy another cursor. The cursor can be on any screen but the x/y/// must be within our screen bounds.pub fn cursorCopy(self: *Screen, other: Cursor, opts: struct { /// Copy the hyperlink from the other cursor. If not set, this will /// clear our current hyperlink. hyperlink: bool = true,}) !void { assert(other.x < self.pages.cols); assert(other.y < self.pages.rows);
// End any currently active hyperlink on our cursor. self.endHyperlink();
const old = self.cursor; self.cursor = other; errdefer self.cursor = old;
// Keep our old style ID so it can be properly cleaned up below. self.cursor.style_id = old.style_id;
// Hyperlinks will be managed separately below. self.cursor.hyperlink_id = 0; self.cursor.hyperlink = null;
// Keep our old page pin and X/Y because: // 1. The old style will need to be cleaned up from the page it's from. // 2. The new position navigated to by `cursorAbsolute` needs to be in our // own screen. self.cursor.page_pin = old.page_pin; self.cursor.x = old.x; self.cursor.y = old.y;
// Call manual style update in order to clean up our old style, if we have // one, and also to load the style from the other cursor, if it had one. try self.manualStyleUpdate();
// Move to the correct location to match the other cursor. self.cursorAbsolute(other.x, other.y);
// If the other cursor had a hyperlink, add it to ours. if (opts.hyperlink and other.hyperlink_id != 0) { // Get the hyperlink from the other cursor's page. const other_page = &other.page_pin.node.data; const other_link = other_page.hyperlink_set.get(other_page.memory, other.hyperlink_id);
const uri = other_link.uri.offset.ptr(other_page.memory)[0..other_link.uri.len]; const id_ = switch (other_link.id) { .explicit => |id| id.offset.ptr(other_page.memory)[0..id.len], .implicit => null, };
// And it to our cursor. self.startHyperlink(uri, id_) catch |err| { // This shouldn't happen because startHyperlink should handle // resizing. This only happens if we're truly out of RAM. Degrade // to forgetting the hyperlink. log.err("failed to update hyperlink on cursor change err={}", .{err}); }; }}
/// Always use this to write to cursor.page_pin.*.////// This specifically handles the case when the new pin is on a different/// page than the old AND we have a style or hyperlink set. In that case,/// we must release our old one and insert the new one, since styles are/// stored per-page.fn cursorChangePin(self: *Screen, new: Pin) void { // Moving the cursor affects text run splitting (ligatures) so // we must mark the old and new page dirty. We do this as long // as the pins are not equal if (!self.cursor.page_pin.eql(new)) { self.cursor.page_pin.markDirty(); new.markDirty(); }
// If our pin is on the same page, then we can just update the pin. // We don't need to migrate any state. if (self.cursor.page_pin.node == new.node) { self.cursor.page_pin.* = new; return; }
// If we have a old style then we need to release it from the old page. const old_style_: ?style.Style = if (self.cursor.style_id == style.default_id) null else self.cursor.style; if (old_style_ != null) { self.cursor.style = .{}; self.manualStyleUpdate() catch unreachable; // Removing a style should never fail }
// If we have a hyperlink then we need to release it from the old page. if (self.cursor.hyperlink != null) { const old_page: *Page = &self.cursor.page_pin.node.data; old_page.hyperlink_set.release(old_page.memory, self.cursor.hyperlink_id); }
// Update our pin to the new page self.cursor.page_pin.* = new;
// On the new page, we need to migrate our style if (old_style_) |old_style| { self.cursor.style = old_style; self.manualStyleUpdate() catch |err| { // This failure should not happen because manualStyleUpdate // handles page splitting, overflow, and more. This should only // happen if we're out of RAM. In this case, we'll just degrade // gracefully back to the default style. log.err("failed to update style on cursor change err={}", .{err}); self.cursor.style = .{}; self.cursor.style_id = 0; }; }
// On the new page, we need to migrate our hyperlink if (self.cursor.hyperlink) |link| { // So we don't attempt to free any memory in the replaced page. self.cursor.hyperlink_id = 0; self.cursor.hyperlink = null;
// Re-add self.startHyperlink(link.uri, switch (link.id) { .explicit => |v| v, .implicit => null, }) catch |err| { // This shouldn't happen because startHyperlink should handle // resizing. This only happens if we're truly out of RAM. Degrade // to forgetting the hyperlink. log.err("failed to update hyperlink on cursor change err={}", .{err}); };
// Remove our old link link.deinit(self.alloc); self.alloc.destroy(link); }}
/// Mark the cursor position as dirty./// TODO: testpub fn cursorMarkDirty(self: *Screen) void { self.cursor.page_pin.markDirty();}
/// Reset the cursor row's soft-wrap state and the cursor's pending wrap./// Also handles clearing the spacer head on the cursor row and resetting/// the wrap_continuation flag on the next row if necessary.////// NOTE(qwerasd): This method is not scrolling region aware, and cannot be/// since it's on Screen not Terminal. This needs to be addressed down the/// line. Not an extremely urgent issue since it's an edge case of an edge/// case, but not ideal.pub fn cursorResetWrap(self: *Screen) void { // Reset the cursor's pending wrap state self.cursor.pending_wrap = false;
const page_row = self.cursor.page_row;
if (!page_row.wrap) return;
// This row does not wrap and the next row is not wrapped to page_row.wrap = false;
if (self.cursor.page_pin.down(1)) |next_row| { next_row.rowAndCell().row.wrap_continuation = false; }
// If the last cell in the row is a spacer head we need to clear it. const cells = self.cursor.page_pin.cells(.all); const cell = cells[self.cursor.page_pin.node.data.size.cols - 1]; if (cell.wide == .spacer_head) { self.clearCells( &self.cursor.page_pin.node.data, page_row, cells[self.cursor.page_pin.node.data.size.cols - 1 ..][0..1], ); }}
/// Options for scrolling the viewport of the terminal grid. The reason/// we have this in addition to PageList.Scroll is because we have additional/// scroll behaviors that are not part of the PageList.Scroll enum.pub const Scroll = union(enum) { /// For all of these, see PageList.Scroll. active, top, pin: Pin, delta_row: isize, delta_prompt: isize,};
/// Scroll the viewport of the terminal grid.pub fn scroll(self: *Screen, behavior: Scroll) void { defer self.assertIntegrity();
// No matter what, scrolling marks our image state as dirty since // it could move placements. If there are no placements or no images // this is still a very cheap operation. self.kitty_images.dirty = true;
switch (behavior) { .active => self.pages.scroll(.{ .active = {} }), .top => self.pages.scroll(.{ .top = {} }), .pin => |p| self.pages.scroll(.{ .pin = p }), .delta_row => |v| self.pages.scroll(.{ .delta_row = v }), .delta_prompt => |v| self.pages.scroll(.{ .delta_prompt = v }), }}
/// See PageList.scrollClear. In addition to that, we reset the cursor/// to be on top.pub fn scrollClear(self: *Screen) !void { defer self.assertIntegrity();
try self.pages.scrollClear(); self.cursorReload();
// No matter what, scrolling marks our image state as dirty since // it could move placements. If there are no placements or no images // this is still a very cheap operation. self.kitty_images.dirty = true;}
/// Returns true if the viewport is scrolled to the bottom of the screen.pub fn viewportIsBottom(self: Screen) bool { return self.pages.viewport == .active;}
/// Erase the region specified by tl and br, inclusive. This will physically/// erase the rows meaning the memory will be reclaimed (if the underlying/// page is empty) and other rows will be shifted up.pub fn eraseRows( self: *Screen, tl: point.Point, bl: ?point.Point,) void { defer self.assertIntegrity();
// Erase the rows self.pages.eraseRows(tl, bl);
// Just to be safe, reset our cursor since it is possible depending // on the points that our active area shifted so our pointers are // invalid. self.cursorReload();}
// Clear the region specified by tl and bl, inclusive. Cleared cells are// colored with the current style background color. This will clear all// cells in the rows.//// If protected is true, the protected flag will be respected and only// unprotected cells will be cleared. Otherwise, all cells will be cleared.pub fn clearRows( self: *Screen, tl: point.Point, bl: ?point.Point, protected: bool,) void { defer self.assertIntegrity();
var it = self.pages.pageIterator(.right_down, tl, bl); while (it.next()) |chunk| { // Mark everything in this chunk as dirty var dirty = chunk.node.data.dirtyBitSet(); dirty.setRangeValue(.{ .start = chunk.start, .end = chunk.end }, true);
for (chunk.rows()) |*row| { const cells_offset = row.cells; const cells_multi: [*]Cell = row.cells.ptr(chunk.node.data.memory); const cells = cells_multi[0..self.pages.cols];
// Clear all cells if (protected) { self.clearUnprotectedCells(&chunk.node.data, row, cells); // We need to preserve other row attributes since we only // cleared unprotected cells. row.cells = cells_offset; } else { self.clearCells(&chunk.node.data, row, cells); row.* = .{ .cells = cells_offset }; } } }}
/// Clear the cells with the blank cell. This takes care to handle/// cleaning up graphemes and styles.pub fn clearCells( self: *Screen, page: *Page, row: *Row, cells: []Cell,) void { // This whole operation does unsafe things, so we just want to assert // the end state. page.pauseIntegrityChecks(true); defer { page.pauseIntegrityChecks(false); page.assertIntegrity(); self.assertIntegrity(); }
if (comptime std.debug.runtime_safety) { // Our row and cells should be within the page. const page_rows = page.rows.ptr(page.memory.ptr); assert(@intFromPtr(row) >= @intFromPtr(&page_rows[0])); assert(@intFromPtr(row) <= @intFromPtr(&page_rows[page.size.rows - 1]));
const row_cells = page.getCells(row); assert(@intFromPtr(&cells[0]) >= @intFromPtr(&row_cells[0])); assert(@intFromPtr(&cells[cells.len - 1]) <= @intFromPtr(&row_cells[row_cells.len - 1])); }
// If this row has graphemes, then we need go through a slow path // and delete the cell graphemes. if (row.grapheme) { for (cells) |*cell| { if (cell.hasGrapheme()) page.clearGrapheme(row, cell); } }
// If we have hyperlinks, we need to clear those. if (row.hyperlink) { for (cells) |*cell| { if (cell.hyperlink) page.clearHyperlink(row, cell); } }
if (row.styled) { for (cells) |*cell| { if (cell.style_id == style.default_id) continue; page.styles.release(page.memory, cell.style_id); }
// If we have no left/right scroll region we can be sure that // the row is no longer styled. if (cells.len == self.pages.cols) row.styled = false; }
if (row.kitty_virtual_placeholder and cells.len == self.pages.cols) { for (cells) |c| { if (c.codepoint() == kitty.graphics.unicode.placeholder) { break; } } else row.kitty_virtual_placeholder = false; }
@memset(cells, self.blankCell());}
/// Clear cells but only if they are not protected.pub fn clearUnprotectedCells( self: *Screen, page: *Page, row: *Row, cells: []Cell,) void { var x0: usize = 0; var x1: usize = 0;
while (x0 < cells.len) clear: { while (cells[x0].protected) { x0 += 1; if (x0 >= cells.len) break :clear; } x1 = x0 + 1; while (x1 < cells.len and !cells[x1].protected) { x1 += 1; } self.clearCells(page, row, cells[x0..x1]); x0 = x1; }
page.assertIntegrity(); self.assertIntegrity();}
/// Clears the prompt lines if the cursor is currently at a prompt. This/// clears the entire line. This is used for resizing when the shell/// handles reflow.////// The cleared cells are not colored with the current style background/// color like other clear functions, because this is a special case used/// for a specific purpose that does not want that behavior.pub fn clearPrompt(self: *Screen) void { var found: ?Pin = null;
// From our cursor, move up and find all prompt lines. var it = self.cursor.page_pin.rowIterator( .left_up, self.pages.pin(.{ .active = .{} }), ); while (it.next()) |p| { const row = p.rowAndCell().row; switch (row.semantic_prompt) { // We are at a prompt but we're not at the start of the prompt. // We mark our found value and continue because the prompt // may be multi-line, unless this is the second time we've // seen an .input marker, in which case we've run into an // earlier prompt. .input => { if (found != null) break; found = p; },
// If we find the prompt then we're done. We are also done // if we find any prompt continuation, because the shells // that send this currently (zsh) cannot redraw every line. .prompt, .prompt_continuation => { found = p; break; },
// If we have command output, then we're most certainly not // at a prompt. Break out of the loop. .command => break,
// If we don't know, we keep searching. .unknown => {}, } }
// If we found a prompt, we clear it. if (found) |top| { var clear_it = top.rowIterator(.right_down, null); while (clear_it.next()) |p| { const row = p.rowAndCell().row; p.node.data.clearCells(row, 0, p.node.data.size.cols); } }}
/// Clean up boundary conditions where a cell will become discontiguous with/// a neighboring cell because either one of them will be moved and/or cleared.////// For performance reasons this is specialized to operate on the cursor row.////// Handles the boundary between the cell at `x` and the cell at `x - 1`.////// So, for example, when moving a region of cells [a, b] (inclusive), call this/// function with `x = a` and `x = b + 1`. It is okay if `x` is out of bounds by/// 1, this will be interpreted correctly.////// DOES NOT MODIFY ROW WRAP STATE! See `cursorResetWrap` for that.////// The following boundary conditions are handled:////// - `x - 1` is a wide character and `x` is a spacer tail:/// o Both cells will be cleared./// o If `x - 1` is the start of the row and was wrapped from a previous row/// then the previous row is checked for a spacer head, which is cleared if/// present.////// - `x == 0` and is a wide character:/// o If the row is a wrap continuation then the previous row will be checked/// for a spacer head, which is cleared if present.////// - `x == cols` and `x - 1` is a spacer head:/// o `x - 1` will be cleared.////// NOTE(qwerasd): This method is not scrolling region aware, and cannot be/// since it's on Screen not Terminal. This needs to be addressed down the/// line. Not an extremely urgent issue since it's an edge case of an edge/// case, but not ideal.pub fn splitCellBoundary( self: *Screen, x: size.CellCountInt,) void { const page = &self.cursor.page_pin.node.data;
page.pauseIntegrityChecks(true); defer page.pauseIntegrityChecks(false);
const cols = self.cursor.page_pin.node.data.size.cols;
// `x` may be up to an INCLUDING `cols`, since that signifies splitting // the boundary to the right of the final cell in the row. assert(x <= cols);
// [ A B C D E F|] // ^ Boundary between final cell and row end. if (x == cols) { if (!self.cursor.page_row.wrap) return;
const cells = self.cursor.page_pin.cells(.all);
// Spacer head at end of wrapped row. if (cells[cols - 1].wide == .spacer_head) { self.clearCells( page, self.cursor.page_row, cells[cols - 1 ..][0..1], ); }
return; }
// [|A B C D E F ] // ^ Boundary between first cell and row start. // // OR // // [ A|B C D E F ] // ^ Boundary between first cell and second cell. // // First cell may be a wrapped wide cell with a spacer // head on the previous row that needs to be cleared. if ((x == 0 or x == 1) and self.cursor.page_row.wrap_continuation) { const cells = self.cursor.page_pin.cells(.all);
// If the first cell in a row is wide the previous row // may have a spacer head which needs to be cleared. if (cells[0].wide == .wide) { if (self.cursor.page_pin.up(1)) |p_row| { const p_rac = p_row.rowAndCell(); const p_cells = p_row.cells(.all); const p_cell = p_cells[p_row.node.data.size.cols - 1]; if (p_cell.wide == .spacer_head) { self.clearCells( &p_row.node.data, p_rac.row, p_cells[p_row.node.data.size.cols - 1 ..][0..1], ); } } } }
// If x is 0 then we're done. if (x == 0) return;
// [ ... X|Y ... ] // ^ Boundary between two cells in the middle of the row. { assert(x > 0); assert(x < cols);
const cells = self.cursor.page_pin.cells(.all);
const left = cells[x - 1]; switch (left.wide) { // There should not be spacer heads in the middle of the row. .spacer_head => unreachable,
// We don't need to do anything for narrow cells or spacer tails. .narrow, .spacer_tail => {},
// A wide char would be split, so must be cleared. .wide => { self.clearCells( page, self.cursor.page_row, cells[x - 1 ..][0..2], ); }, } }}
/// Returns the blank cell to use when doing terminal operations that/// require preserving the bg color.pub fn blankCell(self: *const Screen) Cell { if (self.cursor.style_id == style.default_id) return .{}; return self.cursor.style.bgCell() orelse .{};}
/// Resize the screen. The rows or cols can be bigger or smaller.////// This will reflow soft-wrapped text. If the screen size is getting/// smaller and the maximum scrollback size is exceeded, data will be/// lost from the top of the scrollback.////// If this returns an error, the screen is left in a likely garbage state./// It is very hard to undo this operation without blowing up our memory/// usage. The only way to recover is to reset the screen. The only way/// this really fails is if page allocation is required and fails, which/// probably means the system is in trouble anyways. I'd like to improve this/// in the future but it is not a priority particularly because this scenario/// (resize) is difficult.pub fn resize( self: *Screen, cols: size.CellCountInt, rows: size.CellCountInt,) !void { try self.resizeInternal(cols, rows, true);}
/// Resize the screen without any reflow. In this mode, columns/rows will/// be truncated as they are shrunk. If they are grown, the new space is filled/// with zeros.pub fn resizeWithoutReflow( self: *Screen, cols: size.CellCountInt, rows: size.CellCountInt,) !void { try self.resizeInternal(cols, rows, false);}
/// Resize the screen.fn resizeInternal( self: *Screen, cols: size.CellCountInt, rows: size.CellCountInt, reflow: bool,) !void { defer self.assertIntegrity();
// No matter what we mark our image state as dirty self.kitty_images.dirty = true;
// Release the cursor style while resizing just // in case the cursor ends up on a different page. const cursor_style = self.cursor.style; self.cursor.style = .{}; self.manualStyleUpdate() catch unreachable; defer { // Restore the cursor style. self.cursor.style = cursor_style; self.manualStyleUpdate() catch |err| { // This failure should not happen because manualStyleUpdate // handles page splitting, overflow, and more. This should only // happen if we're out of RAM. In this case, we'll just degrade // gracefully back to the default style. log.err("failed to update style on cursor reload err={}", .{err}); self.cursor.style = .{}; self.cursor.style_id = 0; }; }
// If we have a hyperlink, release it from the old page // and then we need to re-add it to the new page. This needs // to happen because resize below typically reallocates a // new page so the old hyperlink is invalid. const hyperlink_ = self.cursor.hyperlink; if (self.cursor.hyperlink_id != 0) { // Note we do NOT use endHyperlink because we want to keep // our allocated self.cursor.hyperlink valid. var page = &self.cursor.page_pin.node.data; page.hyperlink_set.release(page.memory, self.cursor.hyperlink_id); self.cursor.hyperlink_id = 0; self.cursor.hyperlink = null; }
// We need to insert a tracked pin for our saved cursor so we can // modify its X/Y for reflow. const saved_cursor_pin: ?*Pin = saved_cursor: { const sc = self.saved_cursor orelse break :saved_cursor null; const pin = self.pages.pin(.{ .active = .{ .x = sc.x, .y = sc.y, } }) orelse break :saved_cursor null; break :saved_cursor try self.pages.trackPin(pin); }; defer if (saved_cursor_pin) |p| self.pages.untrackPin(p);
// Perform the resize operation. try self.pages.resize(.{ .rows = rows, .cols = cols, .reflow = reflow, .cursor = .{ .x = self.cursor.x, .y = self.cursor.y }, });
// If we have no scrollback and we shrunk our rows, we must explicitly // erase our history. This is because PageList always keeps at least // a page size of history. if (self.no_scrollback) { self.pages.eraseRows(.{ .history = .{} }, null); }
// If our cursor was updated, we do a full reload so all our cursor // state is correct. self.cursorReload();
// If we reflowed a saved cursor, update it. if (saved_cursor_pin) |p| { // This should never fail because a non-null saved_cursor_pin // implies a non-null saved_cursor. const sc = &self.saved_cursor.?; if (self.pages.pointFromPin(.active, p.*)) |pt| { sc.x = @intCast(pt.active.x); sc.y = @intCast(pt.active.y);
// If we had pending wrap set and we're no longer at the end of // the line, we unset the pending wrap and move the cursor to // reflect the correct next position. if (sc.pending_wrap and sc.x != cols - 1) { sc.pending_wrap = false; sc.x += 1; } } else { // I think this can happen if the screen is resized to be // less rows or less cols and our saved cursor moves outside // the active area. In this case, there isn't anything really // reasonable we can do so we just move the cursor to the // top-left. It may be reasonable to also move the cursor to // match the primary cursor. Any behavior is fine since this is // totally unspecified. sc.x = 0; sc.y = 0; sc.pending_wrap = false; } }
// Fix up our hyperlink if we had one. if (hyperlink_) |link| { self.startHyperlink(link.uri, switch (link.id) { .explicit => |v| v, .implicit => null, }) catch |err| { // This shouldn't happen because startHyperlink should handle // resizing. This only happens if we're truly out of RAM. Degrade // to forgetting the hyperlink. log.err("failed to update hyperlink on resize err={}", .{err}); };
// Remove our old link link.deinit(self.alloc); self.alloc.destroy(link); }}
/// Set a style attribute for the current cursor.////// This can cause a page split if the current page cannot fit this style./// This is the only scenario an error return is possible.pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { switch (attr) { .unset => { self.cursor.style = .{}; },
.bold => { self.cursor.style.flags.bold = true; },
.reset_bold => { // Bold and faint share the same SGR code for this self.cursor.style.flags.bold = false; self.cursor.style.flags.faint = false; },
.italic => { self.cursor.style.flags.italic = true; },
.reset_italic => { self.cursor.style.flags.italic = false; },
.faint => { self.cursor.style.flags.faint = true; },
.underline => |v| { self.cursor.style.flags.underline = v; },
.reset_underline => { self.cursor.style.flags.underline = .none; },
.underline_color => |rgb| { self.cursor.style.underline_color = .{ .rgb = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b, } }; },
.@"256_underline_color" => |idx| { self.cursor.style.underline_color = .{ .palette = idx }; },
.reset_underline_color => { self.cursor.style.underline_color = .none; },
.overline => { self.cursor.style.flags.overline = true; },
.reset_overline => { self.cursor.style.flags.overline = false; },
.blink => { self.cursor.style.flags.blink = true; },
.reset_blink => { self.cursor.style.flags.blink = false; },
.inverse => { self.cursor.style.flags.inverse = true; },
.reset_inverse => { self.cursor.style.flags.inverse = false; },
.invisible => { self.cursor.style.flags.invisible = true; },
.reset_invisible => { self.cursor.style.flags.invisible = false; },
.strikethrough => { self.cursor.style.flags.strikethrough = true; },
.reset_strikethrough => { self.cursor.style.flags.strikethrough = false; },
.direct_color_fg => |rgb| { self.cursor.style.fg_color = .{ .rgb = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b, }, }; },
.direct_color_bg => |rgb| { self.cursor.style.bg_color = .{ .rgb = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b, }, }; },
.@"8_fg" => |n| { self.cursor.style.fg_color = .{ .palette = @intFromEnum(n) }; },
.@"8_bg" => |n| { self.cursor.style.bg_color = .{ .palette = @intFromEnum(n) }; },
.reset_fg => self.cursor.style.fg_color = .none,
.reset_bg => self.cursor.style.bg_color = .none,
.@"8_bright_fg" => |n| { self.cursor.style.fg_color = .{ .palette = @intFromEnum(n) }; },
.@"8_bright_bg" => |n| { self.cursor.style.bg_color = .{ .palette = @intFromEnum(n) }; },
.@"256_fg" => |idx| { self.cursor.style.fg_color = .{ .palette = idx }; },
.@"256_bg" => |idx| { self.cursor.style.bg_color = .{ .palette = idx }; },
.unknown => return, }
try self.manualStyleUpdate();}
/// Call this whenever you manually change the cursor style.pub fn manualStyleUpdate(self: *Screen) !void { var page: *Page = &self.cursor.page_pin.node.data;
// std.log.warn("active styles={}", .{page.styles.count()});
// Release our previous style if it was not default. if (self.cursor.style_id != style.default_id) { page.styles.release(page.memory, self.cursor.style_id); }
// If our new style is the default, just reset to that if (self.cursor.style.default()) { self.cursor.style_id = style.default_id; return; }
// Clear the cursor style ID to prevent weird things from happening // if the page capacity has to be adjusted which would end up calling // manualStyleUpdate again. self.cursor.style_id = style.default_id;
// After setting the style, we need to update our style map. // Note that we COULD lazily do this in print. We should look into // if that makes a meaningful difference. Our priority is to keep print // fast because setting a ton of styles that do nothing is uncommon // and weird. const id = page.styles.add( page.memory, self.cursor.style, ) catch |err| id: { // Our style map is full or needs to be rehashed, // so we allocate a new page, which will rehash, // and double the style capacity for it if it was // full. const node = try self.adjustCapacity( self.cursor.page_pin.node, switch (err) { error.OutOfMemory => .{ .styles = page.capacity.styles * 2 }, error.NeedsRehash => .{}, }, );
page = &node.data; break :id try page.styles.add( page.memory, self.cursor.style, ); }; self.cursor.style_id = id; self.assertIntegrity();}
/// Append a grapheme to the given cell within the current cursor row.pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { defer self.cursor.page_pin.node.data.assertIntegrity(); self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, cell, cp, ) catch |err| switch (err) { error.OutOfMemory => { // We need to determine the actual cell index of the cell so // that after we adjust the capacity we can reload the cell. const cell_idx: usize = cell_idx: { const cells: [*]Cell = @ptrCast(self.cursor.page_cell); const zero: [*]Cell = cells - self.cursor.x; const target: [*]Cell = @ptrCast(cell); const cell_idx = (@intFromPtr(target) - @intFromPtr(zero)) / @sizeOf(Cell); break :cell_idx cell_idx; };
// Adjust our capacity. This will update our cursor page pin and // force us to reload. const original_node = self.cursor.page_pin.node; const new_bytes = original_node.data.capacity.grapheme_bytes * 2; _ = try self.adjustCapacity( original_node, .{ .grapheme_bytes = new_bytes }, );
// The cell pointer is now invalid, so we need to get it from // the reloaded cursor pointers. const reloaded_cell: *Cell = switch (std.math.order(cell_idx, self.cursor.x)) { .eq => self.cursor.page_cell, .lt => self.cursorCellLeft(@intCast(self.cursor.x - cell_idx)), .gt => self.cursorCellRight(@intCast(cell_idx - self.cursor.x)), };
try self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, reloaded_cell, cp, ); }, };}
pub const StartHyperlinkError = Allocator.Error || PageList.AdjustCapacityError;
/// Start the hyperlink state. Future cells will be marked as hyperlinks with/// this state. Note that various terminal operations may clear the hyperlink/// state, such as switching screens (alt screen).pub fn startHyperlink( self: *Screen, uri: []const u8, id_: ?[]const u8,) StartHyperlinkError!void { // Create our pending entry. const link: hyperlink.Hyperlink = .{ .uri = uri, .id = if (id_) |id| .{ .explicit = id, } else implicit: { defer self.cursor.hyperlink_implicit_id += 1; break :implicit .{ .implicit = self.cursor.hyperlink_implicit_id }; }, }; errdefer switch (link.id) { .explicit => {}, .implicit => self.cursor.hyperlink_implicit_id -= 1, };
// Loop until we have enough page memory to add the hyperlink while (true) { if (self.startHyperlinkOnce(link)) { return; } else |err| switch (err) { // An actual self.alloc OOM is a fatal error. error.OutOfMemory => return error.OutOfMemory,
// strings table is out of memory, adjust it up error.StringsOutOfMemory => _ = try self.adjustCapacity( self.cursor.page_pin.node, .{ .string_bytes = self.cursor.page_pin.node.data.capacity.string_bytes * 2 }, ),
// hyperlink set is out of memory, adjust it up error.SetOutOfMemory => _ = try self.adjustCapacity( self.cursor.page_pin.node, .{ .hyperlink_bytes = self.cursor.page_pin.node.data.capacity.hyperlink_bytes * 2 }, ),
// hyperlink set is too full, rehash it error.SetNeedsRehash => _ = try self.adjustCapacity( self.cursor.page_pin.node, .{}, ), }
self.assertIntegrity(); }}
/// This is like startHyperlink but if we have to adjust page capacities/// this returns error.PageAdjusted. This is useful so that we unwind/// all the previous state and try again.fn startHyperlinkOnce( self: *Screen, source: hyperlink.Hyperlink,) (Allocator.Error || Page.InsertHyperlinkError)!void { // End any prior hyperlink self.endHyperlink();
// Allocate our new Hyperlink entry in non-page memory. This // lets us quickly get access to URI, ID. const link = try self.alloc.create(hyperlink.Hyperlink); errdefer self.alloc.destroy(link); link.* = try source.dupe(self.alloc); errdefer link.deinit(self.alloc);
// Insert the hyperlink into page memory var page = &self.cursor.page_pin.node.data; const id: hyperlink.Id = try page.insertHyperlink(link.*);
// Save it all self.cursor.hyperlink = link; self.cursor.hyperlink_id = id;}
/// End the hyperlink state so that future cells aren't part of the/// current hyperlink (if any). This is safe to call multiple times.pub fn endHyperlink(self: *Screen) void { // If we have no hyperlink state then do nothing if (self.cursor.hyperlink_id == 0) { assert(self.cursor.hyperlink == null); return; }
// Release the old hyperlink state. If there are cells using the // hyperlink this will work because the creation creates a reference // and all additional cells create a new reference. This release will // just release our initial reference. // // If the ref count reaches zero the set will not delete the item // immediately; it is kept around in case it is used again (this is // how RefCountedSet works). This causes some memory fragmentation but // is fine because if it is ever pruned the context deleted callback // will be called. var page: *Page = &self.cursor.page_pin.node.data; page.hyperlink_set.release(page.memory, self.cursor.hyperlink_id); self.cursor.hyperlink.?.deinit(self.alloc); self.alloc.destroy(self.cursor.hyperlink.?); self.cursor.hyperlink_id = 0; self.cursor.hyperlink = null;}
/// Set the current hyperlink state on the current cell.pub fn cursorSetHyperlink(self: *Screen) !void { assert(self.cursor.hyperlink_id != 0);
var page = &self.cursor.page_pin.node.data; if (page.setHyperlink( self.cursor.page_row, self.cursor.page_cell, self.cursor.hyperlink_id, )) { // Success, increase the refcount for the hyperlink. page.hyperlink_set.use(page.memory, self.cursor.hyperlink_id); return; } else |err| switch (err) { // hyperlink_map is out of space, realloc the page to be larger error.HyperlinkMapOutOfMemory => { const uri_size = if (self.cursor.hyperlink) |link| link.uri.len else 0;
var string_bytes = page.capacity.string_bytes;
// Attempt to allocate the space that would be required to // insert a new copy of the cursor hyperlink uri in to the // string alloc, since right now adjustCapacity always just // adds an extra copy even if one already exists in the page. // If this alloc fails then we know we also need to grow our // string bytes. // // FIXME: This SUCKS if (page.string_alloc.alloc( u8, page.memory, uri_size, )) |slice| { // We don't bother freeing because we're // about to free the entire page anyway. _ = &slice; } else |_| { // We didn't have enough room, let's just double our // string bytes until there's definitely enough room // for our uri. const before = string_bytes; while (string_bytes - before < uri_size) string_bytes *= 2; }
_ = try self.adjustCapacity( self.cursor.page_pin.node, .{ .hyperlink_bytes = page.capacity.hyperlink_bytes * 2, .string_bytes = string_bytes, }, );
// Retry return try self.cursorSetHyperlink(); }, }}
/// Set the selection to the given selection. If this is a tracked selection/// then the screen will take overnship of the selection. If this is untracked/// then the screen will convert it to tracked internally. This will automatically/// untrack the prior selection (if any).////// Set the selection to null to clear any previous selection.////// This is always recommended over setting `selection` directly. Beyond/// managing memory for you, it also performs safety checks that the selection/// is always tracked.pub fn select(self: *Screen, sel_: ?Selection) !void { const sel = sel_ orelse { self.clearSelection(); return; };
// If this selection is untracked then we track it. const tracked_sel = if (sel.tracked()) sel else try sel.track(self); errdefer if (!sel.tracked()) tracked_sel.deinit(self);
// Untrack prior selection if (self.selection) |*old| old.deinit(self); self.selection = tracked_sel; self.dirty.selection = true;}
/// Same as select(null) but can't fail.pub fn clearSelection(self: *Screen) void { if (self.selection) |*sel| { sel.deinit(self); self.dirty.selection = true; } self.selection = null;}
pub const SelectionString = struct { /// The selection to convert to a string. sel: Selection,
/// If true, trim whitespace around the selection. trim: bool = true,
/// If non-null, a stringmap will be written here. This will use /// the same allocator as the call to selectionString. The string will /// be duplicated here and in the return value so both must be freed. map: ?*StringMap = null,};
/// Returns the raw text associated with a selection. This will unwrap/// soft-wrapped edges. The returned slice is owned by the caller and allocated/// using alloc, not the allocator associated with the screen (unless they match).pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ![:0]const u8 { // Use an ArrayList so that we can grow the array as we go. We // build an initial capacity of just our rows in our selection times // columns. It can be more or less based on graphemes, newlines, etc. var strbuilder = std.ArrayList(u8).init(alloc); defer strbuilder.deinit();
// If we're building a stringmap, create our builder for the pins. const MapBuilder = std.ArrayList(Pin); var mapbuilder: ?MapBuilder = if (opts.map != null) MapBuilder.init(alloc) else null; defer if (mapbuilder) |*b| b.deinit();
const sel_ordered = opts.sel.ordered(self, .forward); const sel_start: Pin = start: { var start: Pin = sel_ordered.start(); const cell = start.rowAndCell().cell; if (cell.wide == .spacer_tail) start.x -= 1; break :start start; }; const sel_end: Pin = end: { var end: Pin = sel_ordered.end(); const cell = end.rowAndCell().cell; switch (cell.wide) { .narrow, .wide => {},
// We can omit the tail .spacer_tail => end.x -= 1,
// With the head we want to include the wrapped wide character. .spacer_head => if (end.down(1)) |p| { end = p; end.x = 0; }, } break :end end; };
var page_it = sel_start.pageIterator(.right_down, sel_end); while (page_it.next()) |chunk| { const rows = chunk.rows(); for (rows, chunk.start.., 0..) |row, y, row_i| { const cells_ptr = row.cells.ptr(chunk.node.data.memory);
const start_x = if ((row_i == 0 or sel_ordered.rectangle) and sel_start.node == chunk.node) sel_start.x else 0; const end_x = if ((row_i == rows.len - 1 or sel_ordered.rectangle) and sel_end.node == chunk.node) sel_end.x + 1 else self.pages.cols;
const cells = cells_ptr[start_x..end_x]; for (cells, start_x..) |*cell, x| { // Skip wide spacers switch (cell.wide) { .narrow, .wide => {}, .spacer_head, .spacer_tail => continue, }
var buf: [4]u8 = undefined; { const raw: u21 = if (cell.hasText()) cell.content.codepoint else 0; const char = if (raw > 0) raw else ' '; const encode_len = try std.unicode.utf8Encode(char, &buf); try strbuilder.appendSlice(buf[0..encode_len]); if (mapbuilder) |*b| { for (0..encode_len) |_| try b.append(.{ .node = chunk.node, .y = @intCast(y), .x = @intCast(x), }); } } if (cell.hasGrapheme()) { const cps = chunk.node.data.lookupGrapheme(cell).?; for (cps) |cp| { const encode_len = try std.unicode.utf8Encode(cp, &buf); try strbuilder.appendSlice(buf[0..encode_len]); if (mapbuilder) |*b| { for (0..encode_len) |_| try b.append(.{ .node = chunk.node, .y = @intCast(y), .x = @intCast(x), }); } } } }
const is_final_row = chunk.node == sel_end.node and y == sel_end.y;
if (!is_final_row and (!row.wrap or sel_ordered.rectangle)) { try strbuilder.append('\n'); if (mapbuilder) |*b| try b.append(.{ .node = chunk.node, .y = @intCast(y), .x = chunk.node.data.size.cols - 1, }); } } }
if (comptime std.debug.runtime_safety) { if (mapbuilder) |b| assert(strbuilder.items.len == b.items.len); }
// If we have a mapbuilder, we need to setup our string map. if (mapbuilder) |*b| { var strclone = try strbuilder.clone(); defer strclone.deinit(); const str = try strclone.toOwnedSliceSentinel(0); errdefer alloc.free(str); const map = try b.toOwnedSlice(); errdefer alloc.free(map); opts.map.?.* = .{ .string = str, .map = map }; }
// Remove any trailing spaces on lines. We could do optimize this by // doing this in the loop above but this isn't very hot path code and // this is simple. if (opts.trim) { var it = std.mem.tokenizeScalar(u8, strbuilder.items, '\n');
// Reset our items. We retain our capacity. Because we're only // removing bytes, we know that the trimmed string must be no longer // than the original string so we copy directly back into our // allocated memory. strbuilder.clearRetainingCapacity(); while (it.next()) |line| { const trimmed = std.mem.trimRight(u8, line, " \t"); const i = strbuilder.items.len; strbuilder.items.len += trimmed.len; std.mem.copyForwards(u8, strbuilder.items[i..], trimmed); try strbuilder.append('\n'); }
// Remove all trailing newlines for (0..strbuilder.items.len) |_| { if (strbuilder.items[strbuilder.items.len - 1] != '\n') break; strbuilder.items.len -= 1; } }
// Get our final string const string = try strbuilder.toOwnedSliceSentinel(0); errdefer alloc.free(string);
return string;}
pub const SelectLine = struct { /// The pin of some part of the line to select. pin: Pin,
/// These are the codepoints to consider whitespace to trim /// from the ends of the selection. whitespace: ?[]const u21 = &.{ 0, ' ', '\t' },
/// If true, line selection will consider semantic prompt /// state changing a boundary. State changing is ANY state /// change. semantic_prompt_boundary: bool = true,};
/// Select the line under the given point. This will select across soft-wrapped/// lines and will omit the leading and trailing whitespace. If the point is/// over whitespace but the line has non-whitespace characters elsewhere, the/// line will be selected.pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection { _ = self;
// Get the current point semantic prompt state since that determines // boundary conditions too. This makes it so that line selection can // only happen within the same prompt state. For example, if you triple // click output, but the shell uses spaces to soft-wrap to the prompt // then the selection will stop prior to the prompt. See issue #1329. const semantic_prompt_state: ?bool = state: { if (!opts.semantic_prompt_boundary) break :state null; const rac = opts.pin.rowAndCell(); break :state rac.row.semantic_prompt.promptOrInput(); };
// The real start of the row is the first row in the soft-wrap. const start_pin: Pin = start_pin: { var it = opts.pin.rowIterator(.left_up, null); var it_prev: Pin = it.next().?; // skip self while (it.next()) |p| { const row = p.rowAndCell().row;
if (!row.wrap) { var copy = it_prev; copy.x = 0; break :start_pin copy; }
if (semantic_prompt_state) |v| { // See semantic_prompt_state comment for why const current_prompt = row.semantic_prompt.promptOrInput(); if (current_prompt != v) { var copy = it_prev; copy.x = 0; break :start_pin copy; } }
it_prev = p; } else { var copy = it_prev; copy.x = 0; break :start_pin copy; } };
// The real end of the row is the final row in the soft-wrap. const end_pin: Pin = end_pin: { var it = opts.pin.rowIterator(.right_down, null); while (it.next()) |p| { const row = p.rowAndCell().row;
if (semantic_prompt_state) |v| { // See semantic_prompt_state comment for why const current_prompt = row.semantic_prompt.promptOrInput(); if (current_prompt != v) { var prev = p.up(1).?; prev.x = p.node.data.size.cols - 1; break :end_pin prev; } }
if (!row.wrap) { var copy = p; copy.x = p.node.data.size.cols - 1; break :end_pin copy; } }
return null; };
// Go forward from the start to find the first non-whitespace character. const start: Pin = start: { const whitespace = opts.whitespace orelse break :start start_pin; var it = start_pin.cellIterator(.right_down, end_pin); while (it.next()) |p| { const cell = p.rowAndCell().cell; if (!cell.hasText()) continue;
// Non-empty means we found it. const this_whitespace = std.mem.indexOfAny( u21, whitespace, &[_]u21{cell.content.codepoint}, ) != null; if (this_whitespace) continue;
break :start p; }
return null; };
// Go backward from the end to find the first non-whitespace character. const end: Pin = end: { const whitespace = opts.whitespace orelse break :end end_pin; var it = end_pin.cellIterator(.left_up, start_pin); while (it.next()) |p| { const cell = p.rowAndCell().cell; if (!cell.hasText()) continue;
// Non-empty means we found it. const this_whitespace = std.mem.indexOfAny( u21, whitespace, &[_]u21{cell.content.codepoint}, ) != null; if (this_whitespace) continue;
break :end p; }
return null; };
return .init(start, end, false);}
/// Return the selection for all contents on the screen. Surrounding/// whitespace is omitted. If there is no selection, this returns null.pub fn selectAll(self: *Screen) ?Selection { const whitespace = &[_]u32{ 0, ' ', '\t' };
const start: Pin = start: { var it = self.pages.cellIterator( .right_down, .{ .screen = .{} }, null, ); while (it.next()) |p| { const cell = p.rowAndCell().cell; if (!cell.hasText()) continue;
// Non-empty means we found it. const this_whitespace = std.mem.indexOfAny( u32, whitespace, &[_]u32{cell.content.codepoint}, ) != null; if (this_whitespace) continue;
break :start p; }
return null; };
const end: Pin = end: { var it = self.pages.cellIterator( .left_up, .{ .screen = .{} }, null, ); while (it.next()) |p| { const cell = p.rowAndCell().cell; if (!cell.hasText()) continue;
// Non-empty means we found it. const this_whitespace = std.mem.indexOfAny( u32, whitespace, &[_]u32{cell.content.codepoint}, ) != null; if (this_whitespace) continue;
break :end p; }
return null; };
return .init(start, end, false);}
/// Select the nearest word to start point that is between start_pt and/// end_pt (inclusive). Because it selects "nearest" to start point, start/// point can be before or after end point.////// TODO: test thispub fn selectWordBetween( self: *Screen, start: Pin, end: Pin,) ?Selection { const dir: PageList.Direction = if (start.before(end)) .right_down else .left_up; var it = start.cellIterator(dir, end); while (it.next()) |pin| { // Boundary conditions switch (dir) { .right_down => if (end.before(pin)) return null, .left_up => if (pin.before(end)) return null, }
// If we found a word, then return it if (self.selectWord(pin)) |sel| return sel; }
return null;}
/// Select the word under the given point. A word is any consecutive series/// of characters that are exclusively whitespace or exclusively non-whitespace./// A selection can span multiple physical lines if they are soft-wrapped.////// This will return null if a selection is impossible. The only scenario/// this happens is if the point pt is outside of the written screen space.pub fn selectWord(self: *Screen, pin: Pin) ?Selection { _ = self;
// Boundary characters for selection purposes const boundary = &[_]u32{ 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', };
// If our cell is empty we can't select a word, because we can't select // areas where the screen is not yet written. const start_cell = pin.rowAndCell().cell; if (!start_cell.hasText()) return null;
// Determine if we are a boundary or not to determine what our boundary is. const expect_boundary = std.mem.indexOfAny( u32, boundary, &[_]u32{start_cell.content.codepoint}, ) != null;
// Go forwards to find our end boundary const end: Pin = end: { var it = pin.cellIterator(.right_down, null); var prev = it.next().?; // Consume one, our start while (it.next()) |p| { const rac = p.rowAndCell(); const cell = rac.cell;
// If we reached an empty cell its always a boundary if (!cell.hasText()) break :end prev;
// If we do not match our expected set, we hit a boundary const this_boundary = std.mem.indexOfAny( u32, boundary, &[_]u32{cell.content.codepoint}, ) != null; if (this_boundary != expect_boundary) break :end prev;
// If we are going to the next row and it isn't wrapped, we // return the previous. if (p.x == p.node.data.size.cols - 1 and !rac.row.wrap) { break :end p; }
prev = p; }
break :end prev; };
// Go backwards to find our start boundary const start: Pin = start: { var it = pin.cellIterator(.left_up, null); var prev = it.next().?; // Consume one, our start while (it.next()) |p| { const rac = p.rowAndCell(); const cell = rac.cell;
// If we are going to the next row and it isn't wrapped, we // return the previous. if (p.x == p.node.data.size.cols - 1 and !rac.row.wrap) { break :start prev; }
// If we reached an empty cell its always a boundary if (!cell.hasText()) break :start prev;
// If we do not match our expected set, we hit a boundary const this_boundary = std.mem.indexOfAny( u32, boundary, &[_]u32{cell.content.codepoint}, ) != null; if (this_boundary != expect_boundary) break :start prev;
prev = p; }
break :start prev; };
return .init(start, end, false);}
/// Select the command output under the given point. The limits of the output/// are determined by semantic prompt information provided by shell integration./// A selection can span multiple physical lines if they are soft-wrapped.////// This will return null if a selection is impossible. The only scenarios/// this happens is if:/// - the point pt is outside of the written screen space./// - the point pt is on a prompt / input line.pub fn selectOutput(self: *Screen, pin: Pin) ?Selection { _ = self;
switch (pin.rowAndCell().row.semantic_prompt) { .input, .prompt_continuation, .prompt => { // Cursor on a prompt line, selection impossible return null; },
else => {}, }
// Go forwards to find our end boundary // We are looking for input start / prompt markers const end: Pin = boundary: { var it = pin.rowIterator(.right_down, null); var it_prev = pin; while (it.next()) |p| { const row = p.rowAndCell().row; switch (row.semantic_prompt) { .input, .prompt_continuation, .prompt => { var copy = it_prev; copy.x = it_prev.node.data.size.cols - 1; break :boundary copy; }, else => {}, }
it_prev = p; }
// Find the last non-blank row it = it_prev.rowIterator(.left_up, null); while (it.next()) |p| { const row = p.rowAndCell().row; const cells = p.node.data.getCells(row); if (Cell.hasTextAny(cells)) { var copy = p; copy.x = p.node.data.size.cols - 1; break :boundary copy; } }
// In this case it means that all our rows are blank. Let's // just return no selection, this is a weird case. return null; };
// Go backwards to find our start boundary // We are looking for output start markers const start: Pin = boundary: { var it = pin.rowIterator(.left_up, null); var it_prev = pin;
// First, iterate until we find the first line of command output while (it.next()) |p| { it_prev = p; const row = p.rowAndCell().row; switch (row.semantic_prompt) { .command => break,
.unknown, .prompt, .prompt_continuation, .input, => {}, } }
// Because the first line of command output may span multiple visual rows we must now // iterate until we find the first row of anything other than command output and then // yield the previous row. while (it.next()) |p| { const row = p.rowAndCell().row; switch (row.semantic_prompt) { .command => {},
.unknown, .prompt, .prompt_continuation, .input, => break :boundary it_prev, } it_prev = p; }
break :boundary it_prev; };
return .init(start, end, false);}
/// Returns the selection bounds for the prompt at the given point. If the/// point is not on a prompt line, this returns null. Note that due to/// the underlying protocol, this will only return the y-coordinates of/// the prompt. The x-coordinates of the start will always be zero and/// the x-coordinates of the end will always be the last column.////// Note that this feature requires shell integration. If shell integration/// is not enabled, this will always return null.pub fn selectPrompt(self: *Screen, pin: Pin) ?Selection { _ = self;
// Ensure that the line the point is on is a prompt. const is_known = switch (pin.rowAndCell().row.semantic_prompt) { .prompt, .prompt_continuation, .input => true, .command => return null,
// We allow unknown to continue because not all shells output any // semantic prompt information for continuation lines. This has the // possibility of making this function VERY slow (we look at all // scrollback) so we should try to avoid this in the future by // setting a flag or something if we have EVER seen a semantic // prompt sequence. .unknown => false, };
// Find the start of the prompt. var saw_semantic_prompt = is_known; const start: Pin = start: { var it = pin.rowIterator(.left_up, null); var it_prev = it.next().?; while (it.next()) |p| { const row = p.rowAndCell().row; switch (row.semantic_prompt) { // A prompt, we continue searching. .prompt, .prompt_continuation, .input => saw_semantic_prompt = true,
// See comment about "unknown" a few lines above. If we have // previously seen a semantic prompt then if we see an unknown // we treat it as a boundary. .unknown => if (saw_semantic_prompt) break :start it_prev,
// Command output or unknown, definitely not a prompt. .command => break :start it_prev, }
it_prev = p; }
break :start it_prev; };
// If we never saw a semantic prompt flag, then we can't trust our // start value and we return null. This scenario usually means that // semantic prompts aren't enabled via the shell. if (!saw_semantic_prompt) return null;
// Find the end of the prompt. const end: Pin = end: { var it = pin.rowIterator(.right_down, null); var it_prev = it.next().?; it_prev.x = it_prev.node.data.size.cols - 1; while (it.next()) |p| { const row = p.rowAndCell().row; switch (row.semantic_prompt) { // A prompt, we continue searching. .prompt, .prompt_continuation, .input => {},
// Command output or unknown, definitely not a prompt. .command, .unknown => break :end it_prev, }
it_prev = p; it_prev.x = it_prev.node.data.size.cols - 1; }
break :end it_prev; };
return .init(start, end, false);}
pub const LineIterator = struct { screen: *const Screen, current: ?Pin = null,
pub fn next(self: *LineIterator) ?Selection { const current = self.current orelse return null; const result = self.screen.selectLine(.{ .pin = current, .whitespace = null, .semantic_prompt_boundary = false, }) orelse { self.current = null; return null; };
self.current = result.end().down(1); return result; }};
/// Returns an iterator to move through the soft-wrapped lines starting/// from pin.pub fn lineIterator(self: *const Screen, start: Pin) LineIterator { return LineIterator{ .screen = self, .current = start, };}
/// Returns the change in x/y that is needed to reach "to" from "from"/// within a prompt. If "to" is before or after the prompt bounds then/// the result will be bounded to the prompt.////// This feature requires shell integration. If shell integration is not/// enabled, this will always return zero for both x and y (no path).pub fn promptPath( self: *Screen, from: Pin, to: Pin,) struct { x: isize, y: isize,} { // Get our prompt bounds assuming "from" is at a prompt. const bounds = self.selectPrompt(from) orelse return .{ .x = 0, .y = 0 };
// Get our actual "to" point clamped to the bounds of the prompt. const to_clamped = if (bounds.contains(self, to)) to else if (to.before(bounds.start())) bounds.start() else bounds.end();
// Convert to points const from_pt = self.pages.pointFromPin(.screen, from).?.screen; const to_pt = self.pages.pointFromPin(.screen, to_clamped).?.screen;
// Basic math to calculate our path. const from_x: isize = @intCast(from_pt.x); const from_y: isize = @intCast(from_pt.y); const to_x: isize = @intCast(to_pt.x); const to_y: isize = @intCast(to_pt.y); return .{ .x = to_x - from_x, .y = to_y - from_y };}
/// Dump the screen to a string. The writer given should be buffered;/// this function does not attempt to efficiently write and generally writes/// one byte at a time.pub fn dumpString( self: *const Screen, writer: anytype, opts: PageList.EncodeUtf8Options,) anyerror!void { try self.pages.encodeUtf8(writer, opts);}
/// You should use dumpString, this is a restricted version mostly for/// legacy and convenience reasons for unit tests.pub fn dumpStringAlloc( self: *const Screen, alloc: Allocator, tl: point.Point,) ![]const u8 { var builder = std.ArrayList(u8).init(alloc); defer builder.deinit();
try self.dumpString(builder.writer(), .{ .tl = self.pages.getTopLeft(tl), .br = self.pages.getBottomRight(tl) orelse return error.UnknownPoint, .unwrap = false, });
return try builder.toOwnedSlice();}
/// You should use dumpString, this is a restricted version mostly for/// legacy and convenience reasons for unit tests.pub fn dumpStringAllocUnwrapped( self: *const Screen, alloc: Allocator, tl: point.Point,) ![]const u8 { var builder = std.ArrayList(u8).init(alloc); defer builder.deinit();
try self.dumpString(builder.writer(), .{ .tl = self.pages.getTopLeft(tl), .br = self.pages.getBottomRight(tl) orelse return error.UnknownPoint, .unwrap = true, });
return try builder.toOwnedSlice();}
/// This is basically a really jank version of Terminal.printString. We/// have to reimplement it here because we want a way to print to the screen/// to test it but don't want all the features of Terminal.pub fn testWriteString(self: *Screen, text: []const u8) !void { const view = try std.unicode.Utf8View.init(text); var iter = view.iterator(); while (iter.nextCodepoint()) |c| { // Explicit newline forces a new row if (c == '\n') { try self.cursorDownOrScroll(); self.cursorHorizontalAbsolute(0); self.cursor.pending_wrap = false; continue; }
const width: usize = if (c <= 0xFF) 1 else @intCast(unicode.table.get(c).width); if (width == 0) { const cell = cell: { var cell = self.cursorCellLeft(1); switch (cell.wide) { .narrow => {}, .wide => {}, .spacer_head => unreachable, .spacer_tail => cell = self.cursorCellLeft(2), }
break :cell cell; };
try self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, cell, c, ); continue; }
if (self.cursor.pending_wrap) { assert(self.cursor.x == self.pages.cols - 1); self.cursor.pending_wrap = false; self.cursor.page_row.wrap = true; try self.cursorDownOrScroll(); self.cursorHorizontalAbsolute(0); self.cursor.page_row.wrap_continuation = true; }
assert(width == 1 or width == 2); switch (width) { 1 => { self.cursor.page_cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = c }, .style_id = self.cursor.style_id, .protected = self.cursor.protected, };
// If we have a hyperlink, add it to the cell. if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
// If we have a ref-counted style, increase. if (self.cursor.style_id != style.default_id) { const page = self.cursor.page_pin.node.data; page.styles.use(page.memory, self.cursor.style_id); self.cursor.page_row.styled = true; } },
2 => { // Need a wide spacer head if (self.cursor.x == self.pages.cols - 1) { self.cursor.page_cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 0 }, .wide = .spacer_head, .protected = self.cursor.protected, };
// If we have a hyperlink, add it to the cell. if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
self.cursor.page_row.wrap = true; try self.cursorDownOrScroll(); self.cursorHorizontalAbsolute(0); self.cursor.page_row.wrap_continuation = true; }
// Write our wide char self.cursor.page_cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = c }, .style_id = self.cursor.style_id, .wide = .wide, .protected = self.cursor.protected, };
// If we have a hyperlink, add it to the cell. if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
// Write our tail self.cursorRight(1); self.cursor.page_cell.* = .{ .content_tag = .codepoint, .content = .{ .codepoint = 0 }, .wide = .spacer_tail, .protected = self.cursor.protected, };
// If we have a hyperlink, add it to the cell. if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
// If we have a ref-counted style, increase twice. if (self.cursor.style_id != style.default_id) { const page = self.cursor.page_pin.node.data; page.styles.use(page.memory, self.cursor.style_id); page.styles.use(page.memory, self.cursor.style_id); self.cursor.page_row.styled = true; } },
else => unreachable, }
if (self.cursor.x + 1 < self.pages.cols) { self.cursorRight(1); } else { self.cursor.pending_wrap = true; } }}
/// Write text that's marked as a semantic prompt.fn testWriteSemanticString(self: *Screen, text: []const u8, semantic_prompt: Row.SemanticPrompt) !void { // Determine the first row using the cursor position. If we know that our // first write is going to start on the next line because of a pending // wrap, we'll proactively start there. const start_y = if (self.cursor.pending_wrap) self.cursor.y + 1 else self.cursor.y;
try self.testWriteString(text);
// Determine the last row that we actually wrote by inspecting the cursor's // position. If we're in the first column, we haven't actually written any // characters to it, so we end at the preceding row instead. const end_y = if (self.cursor.x > 0) self.cursor.y else self.cursor.y - 1;
// Mark the full range of written rows with our semantic prompt. var y = start_y; while (y <= end_y) { const pin = self.pages.pin(.{ .active = .{ .y = y } }).?; pin.rowAndCell().row.semantic_prompt = semantic_prompt; y += 1; }}
test "Screen read and write" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); try testing.expectEqual(@as(style.Id, 0), s.cursor.style_id);
try s.testWriteString("hello, world"); const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("hello, world", str);}
test "Screen read and write newline" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); try testing.expectEqual(@as(style.Id, 0), s.cursor.style_id);
try s.testWriteString("hello\nworld"); const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("hello\nworld", str);}
test "Screen read and write scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 2, 1000); defer s.deinit();
try s.testWriteString("hello\nworld\ntest"); { const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("hello\nworld\ntest", str); } { const str = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(str); try testing.expectEqualStrings("world\ntest", str); }}
test "Screen read and write no scrollback small" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 2, 0); defer s.deinit();
try s.testWriteString("hello\nworld\ntest"); { const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("world\ntest", str); } { const str = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(str); try testing.expectEqualStrings("world\ntest", str); }}
test "Screen read and write no scrollback large" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 2, 0); defer s.deinit();
for (0..1_000) |i| { var buf: [128]u8 = undefined; const str = try std.fmt.bufPrint(&buf, "{}\n", .{i}); try s.testWriteString(str); } try s.testWriteString("1000");
{ const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("999\n1000", str); }}
test "Screen cursorCopy x/y" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 10, 10, 0); defer s.deinit(); s.cursorAbsolute(2, 3); try testing.expect(s.cursor.x == 2); try testing.expect(s.cursor.y == 3);
var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); try s2.cursorCopy(s.cursor, .{}); try testing.expect(s2.cursor.x == 2); try testing.expect(s2.cursor.y == 3); try s2.testWriteString("Hello");
{ const str = try s2.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("\n\n\n Hello", str); }}
test "Screen cursorCopy style deref" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 10, 10, 0); defer s.deinit();
var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); const page = &s2.cursor.page_pin.node.data;
// Bold should create our style try s2.setAttribute(.{ .bold = {} }); try testing.expectEqual(@as(usize, 1), page.styles.count()); try testing.expect(s2.cursor.style.flags.bold);
// Copy default style, should release our style try s2.cursorCopy(s.cursor, .{}); try testing.expect(!s2.cursor.style.flags.bold); try testing.expectEqual(@as(usize, 0), page.styles.count());}
test "Screen cursorCopy style deref new page" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 10, 0); defer s.deinit();
var s2 = try Screen.init(alloc, 10, 10, 2048); defer s2.deinit();
// We need to get the cursor on a new page. const first_page_size = s2.pages.pages.first.?.data.capacity.rows;
// Fill the scrollback with blank lines until // there are only 5 rows left on the first page. s2.pages.pages.first.?.data.pauseIntegrityChecks(true); for (0..first_page_size - 5) |_| { try s2.testWriteString("\n"); } s2.pages.pages.first.?.data.pauseIntegrityChecks(false);
try s2.testWriteString("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
// s2.pages.diagram(...): // // +----------+ = PAGE 0 // ... : : // +-------------+ ACTIVE // 4300 |1 | | 0 // 4301 |2 | | 1 // 4302 |3 | | 2 // 4303 |4 | | 3 // 4304 |5 | | 4 // +----------+ : // +----------+ : = PAGE 1 // 0 |6 | | 5 // 1 |7 | | 6 // 2 |8 | | 7 // 3 |9 | | 8 // 4 |10 | | 9 // : ^ : : = PIN 0 // +----------+ : // +-------------+
// This should be PAGE 1 const page = &s2.cursor.page_pin.node.data;
// It should be the last page in the list. try testing.expectEqual(&s2.pages.pages.last.?.data, page); // It should have a previous page. try testing.expect(s2.cursor.page_pin.node.prev != null);
// The cursor should be at 2, 9 try testing.expect(s2.cursor.x == 2); try testing.expect(s2.cursor.y == 9);
// Bold should create our style in page 1. try s2.setAttribute(.{ .bold = {} }); try testing.expectEqual(@as(usize, 1), page.styles.count()); try testing.expect(s2.cursor.style.flags.bold);
// Copy the cursor for the first screen. This should release // the style from page 1 and move the cursor back to page 0. try s2.cursorCopy(s.cursor, .{}); try testing.expect(!s2.cursor.style.flags.bold); try testing.expectEqual(@as(usize, 0), page.styles.count()); // The page after the page the cursor is now in should be page 1. try testing.expectEqual(page, &s2.cursor.page_pin.node.next.?.data); // The cursor should be at 0, 0 try testing.expect(s2.cursor.x == 0); try testing.expect(s2.cursor.y == 0);}
test "Screen cursorCopy style copy" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 10, 10, 0); defer s.deinit(); try s.setAttribute(.{ .bold = {} });
var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); const page = &s2.cursor.page_pin.node.data; try s2.cursorCopy(s.cursor, .{}); try testing.expect(s2.cursor.style.flags.bold); try testing.expectEqual(@as(usize, 1), page.styles.count());}
test "Screen cursorCopy hyperlink deref" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 10, 10, 0); defer s.deinit();
var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); const page = &s2.cursor.page_pin.node.data;
// Create a hyperlink for the cursor. try s2.startHyperlink("https://example.com/", null); try testing.expectEqual(@as(usize, 1), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id != 0);
// Copy a cursor with no hyperlink, should release our hyperlink. try s2.cursorCopy(s.cursor, .{}); try testing.expectEqual(@as(usize, 0), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id == 0);}
test "Screen cursorCopy hyperlink deref new page" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 10, 0); defer s.deinit();
var s2 = try Screen.init(alloc, 10, 10, 2048); defer s2.deinit();
// We need to get the cursor on a new page. const first_page_size = s2.pages.pages.first.?.data.capacity.rows;
// Fill the scrollback with blank lines until // there are only 5 rows left on the first page. s2.pages.pages.first.?.data.pauseIntegrityChecks(true); for (0..first_page_size - 5) |_| { try s2.testWriteString("\n"); } s2.pages.pages.first.?.data.pauseIntegrityChecks(false);
try s2.testWriteString("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
// s2.pages.diagram(...): // // +----------+ = PAGE 0 // ... : : // +-------------+ ACTIVE // 4300 |1 | | 0 // 4301 |2 | | 1 // 4302 |3 | | 2 // 4303 |4 | | 3 // 4304 |5 | | 4 // +----------+ : // +----------+ : = PAGE 1 // 0 |6 | | 5 // 1 |7 | | 6 // 2 |8 | | 7 // 3 |9 | | 8 // 4 |10 | | 9 // : ^ : : = PIN 0 // +----------+ : // +-------------+
// This should be PAGE 1 const page = &s2.cursor.page_pin.node.data;
// It should be the last page in the list. try testing.expectEqual(&s2.pages.pages.last.?.data, page); // It should have a previous page. try testing.expect(s2.cursor.page_pin.node.prev != null);
// The cursor should be at 2, 9 try testing.expect(s2.cursor.x == 2); try testing.expect(s2.cursor.y == 9);
// Create a hyperlink for the cursor, should be in page 1. try s2.startHyperlink("https://example.com/", null); try testing.expectEqual(@as(usize, 1), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id != 0);
// Copy the cursor for the first screen. This should release // the hyperlink from page 1 and move the cursor back to page 0. try s2.cursorCopy(s.cursor, .{}); try testing.expectEqual(@as(usize, 0), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id == 0); // The page after the page the cursor is now in should be page 1. try testing.expectEqual(page, &s2.cursor.page_pin.node.next.?.data); // The cursor should be at 0, 0 try testing.expect(s2.cursor.x == 0); try testing.expect(s2.cursor.y == 0);}
test "Screen cursorCopy hyperlink copy" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 10, 10, 0); defer s.deinit();
// Create a hyperlink for the cursor. try s.startHyperlink("https://example.com/", null); try testing.expectEqual(@as(usize, 1), s.cursor.page_pin.node.data.hyperlink_set.count()); try testing.expect(s.cursor.hyperlink_id != 0);
var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); const page = &s2.cursor.page_pin.node.data;
try testing.expectEqual(@as(usize, 0), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id == 0);
// Copy the cursor with the hyperlink. try s2.cursorCopy(s.cursor, .{}); try testing.expectEqual(@as(usize, 1), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id != 0);}
test "Screen cursorCopy hyperlink copy disabled" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 10, 10, 0); defer s.deinit();
// Create a hyperlink for the cursor. try s.startHyperlink("https://example.com/", null); try testing.expectEqual(@as(usize, 1), s.cursor.page_pin.node.data.hyperlink_set.count()); try testing.expect(s.cursor.hyperlink_id != 0);
var s2 = try Screen.init(alloc, 10, 10, 0); defer s2.deinit(); const page = &s2.cursor.page_pin.node.data;
try testing.expectEqual(@as(usize, 0), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id == 0);
// Copy the cursor with the hyperlink. try s2.cursorCopy(s.cursor, .{ .hyperlink = false }); try testing.expectEqual(@as(usize, 0), page.hyperlink_set.count()); try testing.expect(s2.cursor.hyperlink_id == 0);}
test "Screen style basics" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); const page = &s.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count());
// Set a new style try s.setAttribute(.{ .bold = {} }); try testing.expect(s.cursor.style_id != 0); try testing.expectEqual(@as(usize, 1), page.styles.count()); try testing.expect(s.cursor.style.flags.bold);
// Set another style, we should still only have one since it was unused try s.setAttribute(.{ .italic = {} }); try testing.expect(s.cursor.style_id != 0); try testing.expectEqual(@as(usize, 1), page.styles.count()); try testing.expect(s.cursor.style.flags.italic);}
test "Screen style reset to default" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); const page = &s.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count());
// Set a new style try s.setAttribute(.{ .bold = {} }); try testing.expect(s.cursor.style_id != 0); try testing.expectEqual(@as(usize, 1), page.styles.count());
// Reset to default try s.setAttribute(.{ .reset_bold = {} }); try testing.expect(s.cursor.style_id == 0); try testing.expectEqual(@as(usize, 0), page.styles.count());}
test "Screen style reset with unset" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); const page = &s.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 0), page.styles.count());
// Set a new style try s.setAttribute(.{ .bold = {} }); try testing.expect(s.cursor.style_id != 0); try testing.expectEqual(@as(usize, 1), page.styles.count());
// Reset to default try s.setAttribute(.{ .unset = {} }); try testing.expect(s.cursor.style_id == 0); try testing.expectEqual(@as(usize, 0), page.styles.count());}
test "Screen clearRows active one line" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit();
try s.testWriteString("hello, world"); s.clearRows(.{ .active = .{} }, null, false); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("", str);}
test "Screen clearRows active multi line" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit();
try s.testWriteString("hello\nworld"); s.clearRows(.{ .active = .{} }, null, false); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("", str);}
test "Screen clearRows active styled line" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit();
try s.setAttribute(.{ .bold = {} }); try s.testWriteString("hello world"); try s.setAttribute(.{ .unset = {} });
// We should have one style const page = &s.cursor.page_pin.node.data; try testing.expectEqual(@as(usize, 1), page.styles.count());
s.clearRows(.{ .active = .{} }, null, false);
// We should have none because active cleared it try testing.expectEqual(@as(usize, 0), page.styles.count());
const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("", str);}
test "Screen clearRows protected" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit();
try s.testWriteString("UNPROTECTED"); s.cursor.protected = true; try s.testWriteString("PROTECTED"); s.cursor.protected = false; try s.testWriteString("UNPROTECTED"); try s.testWriteString("\n"); s.cursor.protected = true; try s.testWriteString("PROTECTED"); s.cursor.protected = false; try s.testWriteString("UNPROTECTED"); s.cursor.protected = true; try s.testWriteString("PROTECTED"); s.cursor.protected = false;
s.clearRows(.{ .active = .{} }, null, true);
const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings(" PROTECTED\nPROTECTED PROTECTED", str);}
test "Screen eraseRows history" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 5, 5, 1000); defer s.deinit();
try s.testWriteString("1\n2\n3\n4\n5\n6");
{ const str = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(str); try testing.expectEqualStrings("2\n3\n4\n5\n6", str); } { const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("1\n2\n3\n4\n5\n6", str); }
s.eraseRows(.{ .history = .{} }, null);
{ const str = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(str); try testing.expectEqualStrings("2\n3\n4\n5\n6", str); } { const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("2\n3\n4\n5\n6", str); }}
test "Screen eraseRows history with more lines" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 5, 5, 1000); defer s.deinit();
try s.testWriteString("A\nB\nC\n1\n2\n3\n4\n5\n6");
{ const str = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(str); try testing.expectEqualStrings("2\n3\n4\n5\n6", str); } { const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("A\nB\nC\n1\n2\n3\n4\n5\n6", str); }
s.eraseRows(.{ .history = .{} }, null);
{ const str = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(str); try testing.expectEqualStrings("2\n3\n4\n5\n6", str); } { const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("2\n3\n4\n5\n6", str); }}
test "Screen eraseRows active partial" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 5, 5, 0); defer s.deinit();
try s.testWriteString("1\n2\n3");
{ const str = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(str); try testing.expectEqualStrings("1\n2\n3", str); }
s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 1 } });
{ const str = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(str); try testing.expectEqualStrings("3", str); } { const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("3", str); }}
test "Screen: clearPrompt" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit();
// Set one of the rows to be a prompt try s.testWriteSemanticString("1ABCD\n", .unknown); try s.testWriteSemanticString("2EFGH\n", .prompt); try s.testWriteSemanticString("3IJKL", .input);
s.clearPrompt();
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD", contents); }}
test "Screen: clearPrompt continuation" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 4, 0); defer s.deinit();
// Set one of the rows to be a prompt followed by a continuation row try s.testWriteSemanticString("1ABCD\n", .unknown); try s.testWriteSemanticString("2EFGH\n", .prompt); try s.testWriteSemanticString("3IJKL\n", .prompt_continuation); try s.testWriteSemanticString("4MNOP", .input);
s.clearPrompt();
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); }}
test "Screen: clearPrompt consecutive inputs" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit();
// Set both rows to be inputs try s.testWriteSemanticString("1ABCD\n", .unknown); try s.testWriteSemanticString("2EFGH\n", .input); try s.testWriteSemanticString("3IJKL", .input);
s.clearPrompt();
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); }}
test "Screen: clearPrompt no prompt" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str);
s.clearPrompt();
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: cursorDown across pages preserves style" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 1); defer s.deinit();
// Scroll down enough to go to another page const start_page = &s.pages.pages.last.?.data; const rem = start_page.capacity.rows; start_page.pauseIntegrityChecks(true); for (0..rem) |_| try s.cursorDownOrScroll(); start_page.pauseIntegrityChecks(false);
// We need our page to change for this test o make sense. If this // assertion fails then the bug is in the test: we should be scrolling // above enough for a new page to show up. { const page = &s.cursor.page_pin.node.data; try testing.expect(start_page != page); }
// Scroll back to the previous page s.cursorUp(1); { const page = &s.cursor.page_pin.node.data; try testing.expect(start_page == page); }
// Go back up, set a style try s.setAttribute(.{ .bold = {} }); { const page = &s.cursor.page_pin.node.data; const styleval = page.styles.get( page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold); }
// Go back down into the next page and we should have that style s.cursorDown(1); { const page = &s.cursor.page_pin.node.data; const styleval = page.styles.get( page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold); }}
test "Screen: cursorUp across pages preserves style" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 1); defer s.deinit();
// Scroll down enough to go to another page const start_page = &s.pages.pages.last.?.data; const rem = start_page.capacity.rows; start_page.pauseIntegrityChecks(true); for (0..rem) |_| try s.cursorDownOrScroll(); start_page.pauseIntegrityChecks(false);
// We need our page to change for this test o make sense. If this // assertion fails then the bug is in the test: we should be scrolling // above enough for a new page to show up. { const page = &s.cursor.page_pin.node.data; try testing.expect(start_page != page); }
// Go back up, set a style try s.setAttribute(.{ .bold = {} }); { const page = &s.cursor.page_pin.node.data; const styleval = page.styles.get( page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold); }
// Go back down into the prev page and we should have that style s.cursorUp(1); { const page = &s.cursor.page_pin.node.data; try testing.expect(start_page == page);
const styleval = page.styles.get( page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold); }}
test "Screen: cursorAbsolute across pages preserves style" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 1); defer s.deinit();
// Scroll down enough to go to another page const start_page = &s.pages.pages.last.?.data; const rem = start_page.capacity.rows; start_page.pauseIntegrityChecks(true); for (0..rem) |_| try s.cursorDownOrScroll(); start_page.pauseIntegrityChecks(false);
// We need our page to change for this test o make sense. If this // assertion fails then the bug is in the test: we should be scrolling // above enough for a new page to show up. { const page = &s.cursor.page_pin.node.data; try testing.expect(start_page != page); }
// Go back up, set a style try s.setAttribute(.{ .bold = {} }); { const page = &s.cursor.page_pin.node.data; const styleval = page.styles.get( page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold); }
// Go back down into the prev page and we should have that style s.cursorAbsolute(1, 1); { const page = &s.cursor.page_pin.node.data; try testing.expect(start_page == page);
const styleval = page.styles.get( page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold); }}
test "Screen: cursorAbsolute to page with insufficient capacity" { // This test checks for a very specific edge case // which previously resulted in memory corruption. // // The conditions for this edge case are as such: // - The cursor has an associated style or other managed memory. // - The cursor moves to a different page. // - The new page is at capacity and must have its capacity adjusted.
const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 1); defer s.deinit();
// Scroll down enough to go to another page const start_page = &s.pages.pages.last.?.data; const rem = start_page.capacity.rows; start_page.pauseIntegrityChecks(true); for (0..rem) |_| try s.cursorDownOrScroll(); start_page.pauseIntegrityChecks(false);
const new_page = &s.cursor.page_pin.node.data;
// We need our page to change for this test to make sense. If this // assertion fails then the bug is in the test: we should be scrolling // above enough for a new page to show up. try testing.expect(start_page != new_page);
// Add styles to the start page until it reaches capacity. { // Pause integrity checks because they're slow and // we're not testing this, this is just setup. start_page.pauseIntegrityChecks(true); defer start_page.pauseIntegrityChecks(false); defer start_page.assertIntegrity();
var n: u24 = 1; while (start_page.styles.add( start_page.memory, .{ .bg_color = .{ .rgb = @bitCast(n) } }, )) |_| n += 1 else |_| {} }
// Set a style on the cursor. try s.setAttribute(.{ .bold = {} }); { const styleval = new_page.styles.get( new_page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold); }
// Go back up into the start page and we should still have that style. s.cursorAbsolute(1, 1); { const cur_page = &s.cursor.page_pin.node.data; // The page we're on now should NOT equal start_page, since its // capacity should have been adjusted, which invalidates our ptr. try testing.expect(start_page != cur_page); // To make sure we DID change pages we check we're not on new_page. try testing.expect(new_page != cur_page);
const styleval = cur_page.styles.get( cur_page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold); }
s.cursor.page_pin.node.data.assertIntegrity(); new_page.assertIntegrity();}
test "Screen: scrolling" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } }); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Scroll down, should still be bottom try s.cursorDownScroll(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 2 } }).?; const cell = list_cell.cell; try testing.expect(cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 155, .g = 0, .b = 0, }, cell.content.color_rgb); }
// Everything is dirty because we have no scrollback try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
// Scrolling to the bottom does nothing s.scroll(.{ .active = {} });
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }}
test "Screen: scrolling with a single-row screen no scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 1, 0); defer s.deinit(); try s.testWriteString("1ABCD");
// Scroll down, should still be bottom try s.cursorDownScroll(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }
// Screen should be dirty try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));}
test "Screen: scrolling with a single-row screen with scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 1, 1); defer s.deinit(); try s.testWriteString("1ABCD");
// Scroll down, should still be bottom try s.cursorDownScroll(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }
// Active should be dirty try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
// Scrollback also dirty because cursor moved from there try testing.expect(s.pages.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
s.scroll(.{ .delta_row = -1 }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD", contents); }}
test "Screen: scrolling across pages preserves style" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 1); defer s.deinit(); try s.setAttribute(.{ .bold = {} }); try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); const start_page = &s.pages.pages.last.?.data;
// Scroll down enough to go to another page const rem = start_page.capacity.rows - start_page.size.rows + 1; start_page.pauseIntegrityChecks(true); for (0..rem) |_| try s.cursorDownOrScroll(); start_page.pauseIntegrityChecks(false);
// We need our page to change for this test o make sense. If this // assertion fails then the bug is in the test: we should be scrolling // above enough for a new page to show up. const page = &s.pages.pages.last.?.data; try testing.expect(start_page != page);
const styleval = page.styles.get( page.memory, s.cursor.style_id, ); try testing.expect(styleval.flags.bold);}
test "Screen: scroll down from 0" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Scrolling up does nothing, but allows it s.scroll(.{ .delta_row = -1 }); try testing.expect(s.pages.viewport == .active);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }}
test "Screen: scrollback various cases" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); try s.cursorDownScroll();
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }
// Scrolling to the bottom s.scroll(.{ .active = {} }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }
// Scrolling back should make it visible again s.scroll(.{ .delta_row = -1 }); try testing.expect(s.pages.viewport != .active); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }
// Scrolling back again should do nothing s.scroll(.{ .delta_row = -1 }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }
// Scrolling to the bottom s.scroll(.{ .active = {} }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }
// Scrolling forward with no grow should do nothing s.scroll(.{ .delta_row = 1 }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }
// Scrolling to the top should work s.scroll(.{ .top = {} }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }
// Should be able to easily clear active area only s.clearRows(.{ .active = .{} }, null, false); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD", contents); }
// Scrolling to the bottom s.scroll(.{ .active = {} }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }}
test "Screen: scrollback with multi-row delta" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 3); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH\n6IJKL");
// Scroll to top s.scroll(.{ .top = {} }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }
// Scroll down multiple s.scroll(.{ .delta_row = 5 }); try testing.expect(s.pages.viewport == .active); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); }}
test "Screen: scrollback empty" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 50); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); s.scroll(.{ .delta_row = 1 }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }}
test "Screen: scrollback doesn't move viewport if not at bottom" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 3); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH");
// First test: we scroll up by 1, so we're not at the bottom anymore. s.scroll(.{ .delta_row = -1 }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); }
// Next, we scroll back down by 1, this grows the scrollback but we // shouldn't move. try s.cursorDownScroll(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); }
// Scroll again, this clears scrollback so we should move viewports // but still see the same thing since our original view fits. try s.cursorDownScroll(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL\n4ABCD", contents); }}
test "Screen: scrolling moves selection" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Select a single line try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 0, .y = 1 } }).?, s.pages.pin(.{ .active = .{ .x = s.pages.cols - 1, .y = 1 } }).?, false, ));
// Scroll down, should still be bottom try s.cursorDownScroll();
// Our selection should've moved up { const sel = s.selection.?; try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = s.pages.cols - 1, .y = 0, } }, s.pages.pointFromPin(.active, sel.end()).?); }
{ // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }
// Scrolling to the bottom does nothing s.scroll(.{ .active = {} });
// Our selection should've stayed the same { const sel = s.selection.?; try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = s.pages.cols - 1, .y = 0, } }, s.pages.pointFromPin(.active, sel.end()).?); }
{ // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }
// Scroll up again try s.cursorDownScroll();
{ // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("3IJKL", contents); }
// Our selection should be null because it left the screen. { const sel = s.selection.?; try testing.expect(s.pages.pointFromPin(.active, sel.start()) == null); try testing.expect(s.pages.pointFromPin(.active, sel.end()) == null); }}
test "Screen: scrolling moves viewport" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); s.scroll(.{ .delta_row = -2 });
{ // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL\n1ABCD", contents); }
{ try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 1, } }, s.pages.pointFromPin(.screen, s.pages.getTopLeft(.viewport))); }}
test "Screen: scrolling when viewport is pruned" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 215, 3, 1); defer s.deinit();
// Write some to create scrollback and move back into our scrollback. try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); s.scroll(.{ .delta_row = -2 });
// Our viewport is now somewhere pinned. Create so much scrollback // that we prune it. try s.testWriteString("\n"); for (0..1000) |_| try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
{ try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, s.pages.getTopLeft(.viewport))); }}
test "Screen: scroll and clear full screen" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 5); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }
try s.scrollClear(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }}
test "Screen: scroll and clear partial screen" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 5); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH");
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); }
try s.scrollClear(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); }}
test "Screen: scroll and clear empty screen" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 5); defer s.deinit(); try s.scrollClear(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }}
test "Screen: scroll and clear ignore blank lines" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 10); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH"); try s.scrollClear(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }
// Move back to top-left s.cursorAbsolute(0, 0);
// Write and clear try s.testWriteString("3ABCD\n"); { const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("3ABCD", contents); }
try s.scrollClear(); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }
// Move back to top-left s.cursorAbsolute(0, 0); try s.testWriteString("X");
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3ABCD\nX", contents); }}
test "Screen: scroll above same page" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 10); defer s.deinit(); try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } }); try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); s.cursorAbsolute(0, 1); s.pages.clearDirty();
// At this point: // +-------------+ ACTIVE // +----------+ : = PAGE 0 // 0 |1ABCD00000| | 0 // 1 |2EFGH00000| | 1 // :^ : : = PIN 0 // 2 |3IJKL00000| | 2 // +----------+ : // +-------------+
try s.cursorScrollAbove();
// +----------+ = PAGE 0 // 0 |1ABCD00000| // +-------------+ ACTIVE // 1 |2EFGH00000| | 0 // 2 | | | 1 // :^ : : = PIN 0 // 3 |3IJKL00000| | 2 // +----------+ : // +-------------+
// try s.pages.diagram(std.io.getStdErr().writer());
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expect(cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 155, .g = 0, .b = 0, }, cell.content.color_rgb); }
// Page 0 row 1 (active row 0) is dirty because the cursor moved off of it. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); // Page 0 row 2 (active row 1) is dirty because it was cleared. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); // Page 0 row 3 (active row 2) is dirty because it's new. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));}
test "Screen: scroll above same page but cursor on previous page" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 5, 10); defer s.deinit();
// We need to get the cursor to a new page const first_page_size = s.pages.pages.first.?.data.capacity.rows; s.pages.pages.first.?.data.pauseIntegrityChecks(true); for (0..first_page_size - 3) |_| try s.testWriteString("\n"); s.pages.pages.first.?.data.pauseIntegrityChecks(false);
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } }); try s.testWriteString("1A\n2B\n3C\n4D\n5E"); s.cursorAbsolute(0, 1); s.pages.clearDirty();
// Ensure we're still on the first page and have a second try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); try testing.expect(s.pages.pages.first.?.next != null);
// At this point: // +----------+ = PAGE 0 // ... : : // +-------------+ ACTIVE // 4305 |1A00000000| | 0 // 4306 |2B00000000| | 1 // :^ : : = PIN 0 // 4307 |3C00000000| | 2 // +----------+ : // +----------+ : = PAGE 1 // 0 |4D00000000| | 3 // 1 |5E00000000| | 4 // +----------+ : // +-------------+
try s.cursorScrollAbove();
// +----------+ = PAGE 0 // ... : : // 4305 |1A00000000| // +-------------+ ACTIVE // 4306 |2B00000000| | 0 // 4307 | | | 1 // :^ : : = PIN 0 // +----------+ : // +----------+ : = PAGE 1 // 0 |3C00000000| | 2 // 1 |4D00000000| | 3 // 2 |5E00000000| | 4 // +----------+ : // +-------------+
// try s.pages.diagram(std.io.getStdErr().writer());
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2B\n\n3C\n4D\n5E", contents); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expect(cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 155, .g = 0, .b = 0, }, cell.content.color_rgb); }
// Page 0's penultimate row is dirty because the cursor moved off of it. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); // The rest of the rows are dirty because they've been modified or are new. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));}
test "Screen: scroll above same page but cursor on previous page last row" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 5, 10); defer s.deinit();
// We need to get the cursor to a new page const first_page_size = s.pages.pages.first.?.data.capacity.rows; s.pages.pages.first.?.data.pauseIntegrityChecks(true); for (0..first_page_size - 2) |_| try s.testWriteString("\n"); s.pages.pages.first.?.data.pauseIntegrityChecks(false);
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } }); try s.testWriteString("1A\n2B\n3C\n4D\n5E"); s.cursorAbsolute(0, 1); s.pages.clearDirty();
// Ensure we're still on the first page and have a second try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); try testing.expect(s.pages.pages.first.?.next != null);
// At this point: // +----------+ = PAGE 0 // ... : : // +-------------+ ACTIVE // 4306 |1A00000000| | 0 // 4307 |2B00000000| | 1 // :^ : : = PIN 0 // +----------+ : // +----------+ : = PAGE 1 // 0 |3C00000000| | 2 // 1 |4D00000000| | 3 // 2 |5E00000000| | 4 // +----------+ : // +-------------+
try s.cursorScrollAbove();
// +----------+ = PAGE 0 // ... : : // 4306 |1A00000000| // +-------------+ ACTIVE // 4307 |2B00000000| | 0 // +----------+ : // +----------+ : = PAGE 1 // 0 | | | 1 // :^ : : = PIN 0 // 1 |3C00000000| | 2 // 2 |4D00000000| | 3 // 3 |5E00000000| | 4 // +----------+ : // +-------------+
// try s.pages.diagram(std.io.getStdErr().writer());
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2B\n\n3C\n4D\n5E", contents); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expect(cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 155, .g = 0, .b = 0, }, cell.content.color_rgb); }
// Page 0's final row is dirty because the cursor moved off of it. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); // Page 1's rows are all dirty because every row was moved. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 4 } }));
// Attempt to clear the style from the cursor and // then assert the integrity of both of our pages. // // This catches a case of memory corruption where the cursor // is moved between pages without accounting for style refs. try s.setAttribute(.{ .reset_bg = {} }); s.pages.pages.first.?.data.assertIntegrity(); s.pages.pages.last.?.data.assertIntegrity();}
test "Screen: scroll above creates new page" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 10); defer s.deinit();
// We need to get the cursor to a new page const first_page_size = s.pages.pages.first.?.data.capacity.rows; s.pages.pages.first.?.data.pauseIntegrityChecks(true); for (0..first_page_size - 3) |_| try s.testWriteString("\n"); s.pages.pages.first.?.data.pauseIntegrityChecks(false);
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } }); try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); s.cursorAbsolute(0, 1); s.pages.clearDirty();
// Ensure we're still on the first page try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?);
// At this point: // +----------+ = PAGE 0 // ... : : // +-------------+ ACTIVE // 4305 |1ABCD00000| | 0 // 4306 |2EFGH00000| | 1 // :^ : : = PIN 0 // 4307 |3IJKL00000| | 2 // +----------+ : // +-------------+ try s.cursorScrollAbove();
// +----------+ = PAGE 0 // ... : : // 4305 |1ABCD00000| // +-------------+ ACTIVE // 4306 |2EFGH00000| | 0 // 4307 | | | 1 // :^ : : = PIN 0 // +----------+ : // +----------+ : = PAGE 1 // 0 |3IJKL00000| | 2 // +----------+ : // +-------------+
// try s.pages.diagram(std.io.getStdErr().writer());
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expect(cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 155, .g = 0, .b = 0, }, cell.content.color_rgb); }
// Page 0's penultimate row is dirty because the cursor moved off of it. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); // Page 0's final row is dirty because it was cleared. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); // Page 1's row is dirty because it's new. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));}
test "Screen: scroll above with cursor on non-final row" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 4, 10); defer s.deinit();
// Get the cursor to be 2 rows above a new page const first_page_size = s.pages.pages.first.?.data.capacity.rows; s.pages.pages.first.?.data.pauseIntegrityChecks(true); for (0..first_page_size - 3) |_| try s.testWriteString("\n"); s.pages.pages.first.?.data.pauseIntegrityChecks(false);
// Write 3 lines of text, forcing the last line into the first // row of a new page. Move our cursor onto the previous page. try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } }); try s.testWriteString("1AB\n2BC\n3DE\n4FG"); s.cursorAbsolute(0, 1); s.pages.clearDirty();
// Ensure we're still on the first page. So our cursor is on the first // page but we have two pages of data. try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?);
// +----------+ = PAGE 0 // ... : : // +-------------+ ACTIVE // 4305 |1AB0000000| | 0 // 4306 |2BC0000000| | 1 // :^ : : = PIN 0 // 4307 |3DE0000000| | 2 // +----------+ : // +----------+ : = PAGE 1 // 0 |4FG0000000| | 3 // +----------+ : // +-------------+ try s.cursorScrollAbove();
// +----------+ = PAGE 0 // ... : : // 4305 |1AB0000000| // +-------------+ ACTIVE // 4306 |2BC0000000| | 0 // 4307 | | | 1 // :^ : : = PIN 0 // +----------+ : // +----------+ : = PAGE 1 // 0 |3DE0000000| | 2 // 1 |4FG0000000| | 3 // +----------+ : // +-------------+ // try s.pages.diagram(std.io.getStdErr().writer());
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2BC\n\n3DE\n4FG", contents); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expect(cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 155, .g = 0, .b = 0, }, cell.content.color_rgb); }
// Page 0's penultimate row is dirty because the cursor moved off of it. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); // Page 0's final row is dirty because it was cleared. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); // Page 1's row is dirty because it's new. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));}
test "Screen: scroll above no scrollback bottom of page" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit();
const first_page_size = s.pages.pages.first.?.data.capacity.rows; s.pages.pages.first.?.data.pauseIntegrityChecks(true); for (0..first_page_size - 3) |_| try s.testWriteString("\n"); s.pages.pages.first.?.data.pauseIntegrityChecks(false);
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } }); try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); s.cursorAbsolute(0, 1); s.pages.clearDirty();
// At this point: // +-------------+ ACTIVE // +----------+ : = PAGE 0 // 0 |1ABCD00000| | 0 // 1 |2EFGH00000| | 1 // :^ : : = PIN 0 // 2 |3IJKL00000| | 2 // +----------+ : // +-------------+
try s.cursorScrollAbove();
// +----------+ = PAGE 0 // 0 |1ABCD00000| // +-------------+ ACTIVE // 1 |2EFGH00000| | 0 // 2 | | | 1 // :^ : : = PIN 0 // 3 |3IJKL00000| | 2 // +----------+ : // +-------------+
//try s.pages.diagram(std.io.getStdErr().writer());
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expect(cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 155, .g = 0, .b = 0, }, cell.content.color_rgb); }
// Page 0 row 1 (active row 0) is dirty because the cursor moved off of it. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); // Page 0 row 2 (active row 1) is dirty because it was cleared. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); // Page 0 row 3 (active row 2) is dirty because it is new. try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));}
test "Screen: clone" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 10); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH"); { const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); } try testing.expectEqual(@as(usize, 5), s.cursor.x); try testing.expectEqual(@as(usize, 1), s.cursor.y);
// Clone var s2 = try s.clone(alloc, .{ .active = .{} }, null); defer s2.deinit(); { const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); } try testing.expectEqual(@as(usize, 5), s2.cursor.x); try testing.expectEqual(@as(usize, 1), s2.cursor.y);
// Write to s1, should not be in s2 try s.testWriteString("\n34567"); { const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n34567", contents); } { const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); } try testing.expectEqual(@as(usize, 5), s2.cursor.x); try testing.expectEqual(@as(usize, 1), s2.cursor.y);}
test "Screen: clone partial" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 10); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH"); { const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); } try testing.expectEqual(@as(usize, 5), s.cursor.x); try testing.expectEqual(@as(usize, 1), s.cursor.y);
// Clone var s2 = try s.clone(alloc, .{ .active = .{ .y = 1 } }, null); defer s2.deinit(); { const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH", contents); }
// Cursor is shifted since we cloned partial try testing.expectEqual(@as(usize, 5), s2.cursor.x); try testing.expectEqual(@as(usize, 0), s2.cursor.y);}
test "Screen: clone partial cursor out of bounds" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 10); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH"); { const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH", contents); } try testing.expectEqual(@as(usize, 5), s.cursor.x); try testing.expectEqual(@as(usize, 1), s.cursor.y);
// Clone var s2 = try s.clone( alloc, .{ .active = .{ .y = 0 } }, .{ .active = .{ .y = 0 } }, ); defer s2.deinit(); { const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD", contents); }
// Cursor is shifted since we cloned partial try testing.expectEqual(@as(usize, 0), s2.cursor.x); try testing.expectEqual(@as(usize, 0), s2.cursor.y);}
test "Screen: clone contains full selection" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Select a single line try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 0, .y = 1 } }).?, s.pages.pin(.{ .active = .{ .x = s.pages.cols - 1, .y = 1 } }).?, false, ));
// Clone var s2 = try s.clone( alloc, .{ .active = .{} }, null, ); defer s2.deinit();
// Our selection should remain valid { const sel = s2.selection.?; try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 1, } }, s2.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = s2.pages.cols - 1, .y = 1, } }, s2.pages.pointFromPin(.active, sel.end()).?); }}
test "Screen: clone contains none of selection" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Select a single line try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .active = .{ .x = s.pages.cols - 1, .y = 0 } }).?, false, ));
// Clone var s2 = try s.clone( alloc, .{ .active = .{ .y = 1 } }, null, ); defer s2.deinit();
// Our selection should be null try testing.expect(s2.selection == null);}
test "Screen: clone contains selection start cutoff" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Select a single line try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .active = .{ .x = s.pages.cols - 1, .y = 1 } }).?, false, ));
// Clone var s2 = try s.clone( alloc, .{ .active = .{ .y = 1 } }, null, ); defer s2.deinit();
// Our selection should remain valid { const sel = s2.selection.?; try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 0, } }, s2.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = s2.pages.cols - 1, .y = 0, } }, s2.pages.pointFromPin(.active, sel.end()).?); }}
test "Screen: clone contains selection end cutoff" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Select a single line try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 0, .y = 1 } }).?, s.pages.pin(.{ .active = .{ .x = 2, .y = 2 } }).?, false, ));
// Clone var s2 = try s.clone( alloc, .{ .active = .{ .y = 0 } }, .{ .active = .{ .y = 1 } }, ); defer s2.deinit();
// Our selection should remain valid { const sel = s2.selection.?; try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 1, } }, s2.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = s2.pages.cols - 1, .y = 2, } }, s2.pages.pointFromPin(.active, sel.end()).?); }}
test "Screen: clone contains selection end cutoff reversed" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
// Select a single line try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 2, .y = 2 } }).?, s.pages.pin(.{ .active = .{ .x = 0, .y = 1 } }).?, false, ));
// Clone var s2 = try s.clone( alloc, .{ .active = .{ .y = 0 } }, .{ .active = .{ .y = 1 } }, ); defer s2.deinit();
// Our selection should remain valid { const sel = s2.selection.?; try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 1, } }, s2.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = s2.pages.cols - 1, .y = 2, } }, s2.pages.pointFromPin(.active, sel.end()).?); }}
test "Screen: clone contains subset of selection" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 4, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD");
// Select the full screen try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .active = .{ .x = 0, .y = 3 } }).?, false, ));
// Clone var s2 = try s.clone( alloc, .{ .active = .{ .y = 1 } }, .{ .active = .{ .y = 2 } }, ); defer s2.deinit();
// Our selection should remain valid { const sel = s2.selection.?; try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 0, } }, s2.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = s2.pages.cols - 1, .y = 3, } }, s2.pages.pointFromPin(.active, sel.end()).?); }}
test "Screen: clone contains subset of rectangle selection" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 4, 1); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD");
// Select the full screen from x=1 to x=3 try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, s.pages.pin(.{ .active = .{ .x = 3, .y = 3 } }).?, true, ));
// Clone var s2 = try s.clone( alloc, .{ .active = .{ .y = 1 } }, .{ .active = .{ .y = 2 } }, ); defer s2.deinit();
// Our selection should remain valid and be properly clipped // preserving the columns of the start and end points of the // selection. { const sel = s2.selection.?; try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 0, } }, s2.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 3, .y = 3, } }, s2.pages.pointFromPin(.active, sel.end()).?); }}
test "Screen: clone basic" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
{ var s2 = try s.clone( alloc, .{ .active = .{ .y = 1 } }, .{ .active = .{ .y = 1 } }, ); defer s2.deinit();
// Test our contents rotated const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH", contents); }
{ var s2 = try s.clone( alloc, .{ .active = .{ .y = 1 } }, .{ .active = .{ .y = 2 } }, ); defer s2.deinit();
// Test our contents rotated const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }}
test "Screen: clone empty viewport" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit();
{ var s2 = try s.clone( alloc, .{ .viewport = .{ .y = 0 } }, .{ .viewport = .{ .y = 0 } }, ); defer s2.deinit();
// Test our contents rotated const contents = try s2.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }}
test "Screen: clone one line viewport" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); try s.testWriteString("1ABC");
{ var s2 = try s.clone( alloc, .{ .viewport = .{ .y = 0 } }, .{ .viewport = .{ .y = 0 } }, ); defer s2.deinit();
// Test our contents const contents = try s2.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABC", contents); }}
test "Screen: clone empty active" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit();
{ var s2 = try s.clone( alloc, .{ .active = .{ .y = 0 } }, .{ .active = .{ .y = 0 } }, ); defer s2.deinit();
// Test our contents rotated const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }}
test "Screen: clone one line active with extra space" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); try s.testWriteString("1ABC");
{ var s2 = try s.clone( alloc, .{ .active = .{ .y = 0 } }, null, ); defer s2.deinit();
// Test our contents rotated const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABC", contents); }}
test "Screen: clear history with no history" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 3); defer s.deinit(); try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); try testing.expect(s.pages.viewport == .active); s.eraseRows(.{ .history = .{} }, null); try testing.expect(s.pages.viewport == .active); { // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); } { // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); }}
test "Screen: clear history" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 3); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH\n6IJKL"); try testing.expect(s.pages.viewport == .active);
// Scroll to top s.scroll(.{ .top = {} }); { // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL", contents); }
s.eraseRows(.{ .history = .{} }, null); try testing.expect(s.pages.viewport == .active); { // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); } { // Test our contents rotated const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("4ABCD\n5EFGH\n6IJKL", contents); }}
test "Screen: clear above cursor" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 10, 3); defer s.deinit(); try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); s.clearRows( .{ .active = .{ .y = 0 } }, .{ .active = .{ .y = s.cursor.y - 1 } }, false, ); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("\n\n6IJKL", contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("\n\n6IJKL", contents); }
try testing.expectEqual(@as(usize, 5), s.cursor.x); try testing.expectEqual(@as(usize, 2), s.cursor.y);}
test "Screen: clear above cursor with history" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 3); defer s.deinit(); try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); s.clearRows( .{ .active = .{ .y = 0 } }, .{ .active = .{ .y = s.cursor.y - 1 } }, false, ); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("\n\n6IJKL", contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n2EFGH\n3IJKL\n\n\n6IJKL", contents); }
try testing.expectEqual(@as(usize, 5), s.cursor.x); try testing.expectEqual(@as(usize, 2), s.cursor.y);}
test "Screen: resize (no reflow) more rows" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str);
// Resize try s.resizeWithoutReflow(10, 10); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: resize (no reflow) less rows" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str); try testing.expectEqual(5, s.cursor.x); try testing.expectEqual(2, s.cursor.y); try s.resizeWithoutReflow(10, 2);
// Since we shrunk, we should adjust our cursor try testing.expectEqual(5, s.cursor.x); try testing.expectEqual(1, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2EFGH\n3IJKL", contents); }}
test "Screen: resize (no reflow) less rows trims blank lines" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); const str = "1ABCD"; try s.testWriteString(str);
// Write only a background color into the remaining rows for (1..s.pages.rows) |y| { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = @intCast(y), } }).?; list_cell.cell.* = .{ .content_tag = .bg_color_rgb, .content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }, }; }
const cursor = s.cursor; try s.resizeWithoutReflow(6, 2);
// Cursor should not move try testing.expectEqual(cursor.x, s.cursor.x); try testing.expectEqual(cursor.y, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD", contents); }}
test "Screen: resize (no reflow) more rows trims blank lines" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); const str = "1ABCD"; try s.testWriteString(str);
// Write only a background color into the remaining rows for (1..s.pages.rows) |y| { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = @intCast(y), } }).?; list_cell.cell.* = .{ .content_tag = .bg_color_rgb, .content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }, }; }
const cursor = s.cursor; try s.resizeWithoutReflow(10, 7);
// Cursor should not move try testing.expectEqual(cursor.x, s.cursor.x); try testing.expectEqual(cursor.y, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD", contents); }}
test "Screen: resize (no reflow) more cols" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str); try s.resizeWithoutReflow(20, 3);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: resize (no reflow) less cols" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str); try s.resizeWithoutReflow(4, 3);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1ABC\n2EFG\n3IJK"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize (no reflow) more rows with scrollback cursor end" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 7, 3, 2); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; try s.testWriteString(str); try s.resizeWithoutReflow(7, 10);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: resize (no reflow) less rows with scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 7, 3, 2); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; try s.testWriteString(str); try s.resizeWithoutReflow(7, 2);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }}
// https://github.com/mitchellh/ghostty/issues/1030test "Screen: resize (no reflow) less rows with empty trailing" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5); defer s.deinit(); const str = "1\n2\n3\n4\n5\n6\n7\n8"; try s.testWriteString(str); try s.scrollClear(); s.cursorAbsolute(0, 0); try s.testWriteString("A\nB");
const cursor = s.cursor; try s.resizeWithoutReflow(5, 2); try testing.expectEqual(cursor.x, s.cursor.x); try testing.expectEqual(cursor.y, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("A\nB", contents); }}
test "Screen: resize (no reflow) more rows with soft wrapping" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 2, 3, 3); defer s.deinit(); const str = "1A2B\n3C4E\n5F6G"; try s.testWriteString(str);
// Every second row should be wrapped for (0..6) |y| { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = @intCast(y), } }).?; const row = list_cell.row; const wrapped = (y % 2 == 0); try testing.expectEqual(wrapped, row.wrap); }
// Resize try s.resizeWithoutReflow(2, 10); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1A\n2B\n3C\n4E\n5F\n6G"; try testing.expectEqualStrings(expected, contents); }
// Every second row should be wrapped for (0..6) |y| { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = @intCast(y), } }).?; const row = list_cell.row; const wrapped = (y % 2 == 0); try testing.expectEqual(wrapped, row.wrap); }}
test "Screen: resize more rows no scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str); const cursor = s.cursor; try s.resize(5, 10);
// Cursor should not move try testing.expectEqual(cursor.x, s.cursor.x); try testing.expectEqual(cursor.y, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: resize more rows with empty scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 10); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str); const cursor = s.cursor; try s.resize(5, 10);
// Cursor should not move try testing.expectEqual(cursor.x, s.cursor.x); try testing.expectEqual(cursor.y, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: resize more rows with populated scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL\n4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }
// Set our cursor to be on the "4" s.cursorAbsolute(0, 1); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint); }
// Resize try s.resize(5, 10);
// Cursor should still be on the "4" { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint); }
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL\n4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize more cols no reflow" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str);
const cursor = s.cursor; try s.resize(10, 3);
// Cursor should not move try testing.expectEqual(cursor.x, s.cursor.x); try testing.expectEqual(cursor.y, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
// https://github.com/mitchellh/ghostty/issues/272#issuecomment-1676038963test "Screen: resize more cols perfect split" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD2EFGH3IJKL"; try s.testWriteString(str); try s.resize(10, 3);
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("1ABCD2EFGH\n3IJKL", contents); }}
// https://github.com/mitchellh/ghostty/issues/1159test "Screen: resize (no reflow) more cols with scrollback scrolled up" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5); defer s.deinit(); const str = "1\n2\n3\n4\n5\n6\n7\n8"; try s.testWriteString(str);
// Cursor at bottom try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y);
s.scroll(.{ .delta_row = -4 }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2\n3\n4", contents); }
try s.resize(8, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }
// Cursor remains at bottom try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y);}
// https://github.com/mitchellh/ghostty/issues/1159test "Screen: resize (no reflow) less cols with scrollback scrolled up" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5); defer s.deinit(); const str = "1\n2\n3\n4\n5\n6\n7\n8"; try s.testWriteString(str);
// Cursor at bottom try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y);
s.scroll(.{ .delta_row = -4 }); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("2\n3\n4", contents); }
try s.resize(4, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .active = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("6\n7\n8", contents); }
// Cursor remains at bottom try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y);
// Old implementation doesn't do this but it makes sense to me: // { // const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); // defer alloc.free(contents); // try testing.expectEqualStrings("2\n3\n4", contents); // }}
test "Screen: resize more cols no reflow preserves semantic prompt" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit();
// Set one of the rows to be a prompt try s.testWriteSemanticString("1ABCD\n", .unknown); try s.testWriteSemanticString("2EFGH\n", .prompt); try s.testWriteSemanticString("3IJKL", .unknown);
try s.resize(10, 3);
const expected = "1ABCD\n2EFGH\n3IJKL"; { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(expected, contents); }
// Our one row should still be a semantic prompt, the others should not. { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; try testing.expect(list_cell.row.semantic_prompt == .unknown); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; try testing.expect(list_cell.row.semantic_prompt == .prompt); } { const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 2 } }).?; try testing.expect(list_cell.row.semantic_prompt == .unknown); }}
test "Screen: resize more cols with reflow that fits full width" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD2EFGH\n3IJKL"; try s.testWriteString(str);
// Verify we soft wrapped { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1ABCD\n2EFGH\n3IJKL"; try testing.expectEqualStrings(expected, contents); }
// Let's put our cursor on row 2, where the soft wrap is s.cursorAbsolute(0, 1); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '2'), list_cell.cell.content.codepoint); }
// Resize and verify we undid the soft wrap because we have space now try s.resize(10, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }
// Our cursor should've moved try testing.expectEqual(@as(usize, 5), s.cursor.x); try testing.expectEqual(@as(usize, 0), s.cursor.y);}
test "Screen: resize more cols with reflow that ends in newline" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 6, 3, 0); defer s.deinit(); const str = "1ABCD2EFGH\n3IJKL"; try s.testWriteString(str);
// Verify we soft wrapped { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1ABCD2\nEFGH\n3IJKL"; try testing.expectEqualStrings(expected, contents); }
// Let's put our cursor on the last row s.cursorAbsolute(0, 2); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint); }
// Resize and verify we undid the soft wrap because we have space now try s.resize(10, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }
// Our cursor should still be on the 3 { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint); }}
test "Screen: resize more cols with reflow that forces more wrapping" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD2EFGH\n3IJKL"; try s.testWriteString(str);
// Let's put our cursor on row 2, where the soft wrap is s.cursorAbsolute(0, 1); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '2'), list_cell.cell.content.codepoint); }
// Verify we soft wrapped { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1ABCD\n2EFGH\n3IJKL"; try testing.expectEqualStrings(expected, contents); }
// Resize and verify we undid the soft wrap because we have space now try s.resize(7, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1ABCD2E\nFGH\n3IJKL"; try testing.expectEqualStrings(expected, contents); }
// Our cursor should've moved try testing.expectEqual(@as(size.CellCountInt, 5), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.y);}
test "Screen: resize more cols with reflow that unwraps multiple times" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD2EFGH3IJKL"; try s.testWriteString(str);
// Let's put our cursor on row 2, where the soft wrap is s.cursorAbsolute(0, 2); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint); }
// Verify we soft wrapped { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1ABCD\n2EFGH\n3IJKL"; try testing.expectEqualStrings(expected, contents); }
// Resize and verify we undid the soft wrap because we have space now try s.resize(15, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1ABCD2EFGH3IJKL"; try testing.expectEqualStrings(expected, contents); }
// Our cursor should've moved try testing.expectEqual(@as(size.CellCountInt, 10), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.y);}
test "Screen: resize more cols with populated scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD5EFGH"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL\n4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }
// // Set our cursor to be on the "5" s.cursorAbsolute(0, 2); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '5'), list_cell.cell.content.codepoint); }
// Resize try s.resize(10, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "2EFGH\n3IJKL\n4ABCD5EFGH"; try testing.expectEqualStrings(expected, contents); }
// Cursor should still be on the "5" { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u21, '5'), list_cell.cell.content.codepoint); }}
test "Screen: resize more cols with reflow" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 2, 3, 5); defer s.deinit(); const str = "1ABC\n2DEF\n3ABC\n4DEF"; try s.testWriteString(str);
// Let's put our cursor on row 2, where the soft wrap is s.cursorAbsolute(0, 2); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u32, 'E'), list_cell.cell.content.codepoint); }
// Verify we soft wrapped { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "BC\n4D\nEF"; try testing.expectEqualStrings(expected, contents); }
// Resize and verify we undid the soft wrap because we have space now try s.resize(7, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "1ABC\n2DEF\n3ABC\n4DEF"; try testing.expectEqualStrings(expected, contents); }
// Our cursor should've moved try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y);}
test "Screen: resize more rows and cols with wrapping" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 2, 4, 0); defer s.deinit(); const str = "1A2B\n3C4D"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1A\n2B\n3C\n4D"; try testing.expectEqualStrings(expected, contents); }
try s.resize(5, 10);
// Cursor should move due to wrapping try testing.expectEqual(@as(size.CellCountInt, 3), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: resize less rows no scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str);
s.cursorAbsolute(0, 0); const cursor = s.cursor; try s.resize(5, 1);
// Cursor should not move try testing.expectEqual(cursor.x, s.cursor.x); try testing.expectEqual(cursor.y, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "3IJKL"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize less rows moving cursor" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str);
// Put our cursor on the last line s.cursorAbsolute(1, 2); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u32, 'I'), list_cell.cell.content.codepoint); }
// Resize try s.resize(5, 1);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "3IJKL"; try testing.expectEqualStrings(expected, contents); }
// Cursor should be on the last line try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.y);}
test "Screen: resize less rows with empty scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 10); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str); try s.resize(5, 1);
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize less rows with populated scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL\n4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }
// Resize try s.resize(5, 1);
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "5EFGH"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize less rows with full scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 3); defer s.deinit(); const str = "00000\n1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL\n4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }
try testing.expectEqual(@as(size.CellCountInt, 4), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y);
// Resize try s.resize(5, 2);
// Cursor should stay in the same relative place (bottom of the // screen, same character). try testing.expectEqual(@as(size.CellCountInt, 4), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "00000\n1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize less cols no reflow" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1AB\n2EF\n3IJ"; try s.testWriteString(str);
s.cursorAbsolute(0, 0); const cursor = s.cursor; try s.resize(3, 3);
// Cursor should not move try testing.expectEqual(cursor.x, s.cursor.x); try testing.expectEqual(cursor.y, s.cursor.y);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: resize less cols with reflow but row space" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 1); defer s.deinit(); const str = "1ABCD"; try s.testWriteString(str);
// Put our cursor on the end s.cursorAbsolute(4, 0); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u32, 'D'), list_cell.cell.content.codepoint); }
try s.resize(3, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "1AB\nCD"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "1AB\nCD"; try testing.expectEqualStrings(expected, contents); }
// Cursor should be on the last line try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.y);}
test "Screen: resize less cols with reflow with trimmed rows" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "3IJKL\n4ABCD\n5EFGH"; try s.testWriteString(str); try s.resize(3, 3);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "CD\n5EF\nGH"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "CD\n5EF\nGH"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize less cols with reflow with trimmed rows and scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 1); defer s.deinit(); const str = "3IJKL\n4ABCD\n5EFGH"; try s.testWriteString(str); try s.resize(3, 3);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "CD\n5EF\nGH"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "3IJ\nKL\n4AB\nCD\n5EF\nGH"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize less cols with reflow previously wrapped" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "3IJKL4ABCD5EFGH"; try s.testWriteString(str);
// Check { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "3IJKL\n4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }
try s.resize(3, 3);
// { // const contents = try s.testString(alloc, .viewport); // defer alloc.free(contents); // const expected = "CD\n5EF\nGH"; // try testing.expectEqualStrings(expected, contents); // } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "ABC\nD5E\nFGH"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: resize less cols with reflow and scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5); defer s.deinit(); const str = "1A\n2B\n3C\n4D\n5E"; try s.testWriteString(str);
// Put our cursor on the end s.cursorAbsolute(1, s.pages.rows - 1); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u32, 'E'), list_cell.cell.content.codepoint); }
try s.resize(3, 3);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3C\n4D\n5E"; try testing.expectEqualStrings(expected, contents); }
// Cursor should be on the last line try testing.expectEqual(@as(size.CellCountInt, 1), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y);}
test "Screen: resize less cols with reflow previously wrapped and scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 2); defer s.deinit(); const str = "1ABCD2EFGH3IJKL4ABCD5EFGH"; try s.testWriteString(str);
// Check { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "3IJKL\n4ABCD\n5EFGH"; try testing.expectEqualStrings(expected, contents); }
// Put our cursor on the end s.cursorAbsolute(s.pages.cols - 1, s.pages.rows - 1); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u32, 'H'), list_cell.cell.content.codepoint); }
try s.resize(3, 3);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "CD5\nEFG\nH"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "1AB\nCD2\nEFG\nH3I\nJKL\n4AB\nCD5\nEFG\nH"; try testing.expectEqualStrings(expected, contents); }
// Cursor should be on the last line try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y); { const list_cell = s.pages.getCell(.{ .active = .{ .x = s.cursor.x, .y = s.cursor.y, } }).?; try testing.expectEqual(@as(u32, 'H'), list_cell.cell.content.codepoint); }}
test "Screen: resize less cols with scrollback keeps cursor row" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 5); defer s.deinit(); const str = "1A\n2B\n3C\n4D\n5E"; try s.testWriteString(str);
// Lets do a scroll and clear operation try s.scrollClear();
// Move our cursor to the beginning s.cursorAbsolute(0, 0);
try s.resize(3, 3);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = ""; try testing.expectEqualStrings(expected, contents); }
// Cursor should be on the last line try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.x); try testing.expectEqual(@as(size.CellCountInt, 0), s.cursor.y);}
test "Screen: resize more rows, less cols with reflow with scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 3); defer s.deinit(); const str = "1ABCD\n2EFGH3IJKL\n4MNOP"; try s.testWriteString(str);
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "1ABCD\n2EFGH\n3IJKL\n4MNOP"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "2EFGH\n3IJKL\n4MNOP"; try testing.expectEqualStrings(expected, contents); }
try s.resize(2, 10);
{ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); const expected = "BC\nD\n2E\nFG\nH3\nIJ\nKL\n4M\nNO\nP"; try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); const expected = "1A\nBC\nD\n2E\nFG\nH3\nIJ\nKL\n4M\nNO\nP"; try testing.expectEqualStrings(expected, contents); }}
// This seems like it should work fine but for some reason in practice// in the initial implementation I found this bug! This is a regression// test for that.test "Screen: resize more rows then shrink again" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 10); defer s.deinit(); const str = "1ABC"; try s.testWriteString(str);
// Grow try s.resize(5, 10); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }
// Shrink try s.resize(5, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }
// Grow again try s.resize(5, 10); { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); }}
test "Screen: resize less cols to eliminate wide char" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 2, 1, 0); defer s.deinit(); const str = "😀"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint); }
// Resize to 1 column can't fit a wide char. So it should be deleted. try s.resize(1, 1); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); } { const list_cell = s.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); }}
test "Screen: resize less cols to wrap wide char" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 3, 3, 0); defer s.deinit(); const str = "x😀"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }
try s.resize(2, 3); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("x\n😀", contents); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); try testing.expect(list_cell.row.wrap); }}
test "Screen: resize less cols to eliminate wide char with row space" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 2, 2, 0); defer s.deinit(); const str = "😀"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }
try s.resize(1, 2); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("", contents); }}
test "Screen: resize more cols with wide spacer head" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 3, 2, 0); defer s.deinit(); const str = " 😀"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(" \n😀", contents); }
// So this is the key point: we end up with a wide spacer head at // the end of row 1, then the emoji, then a wide spacer tail on row 2. // We should expect that if we resize to more cols, the wide spacer // head is replaced with the emoji. { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }
try s.resize(4, 2); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Screen: resize more cols with wide spacer head multiple lines" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 3, 3, 0); defer s.deinit(); const str = "xxxyy😀"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("xxx\nyy\n😀", contents); }
// Similar to the "wide spacer head" test, but this time we'er going // to increase our columns such that multiple rows are unwrapped. { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 2, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 2 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 2 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }
try s.resize(8, 2); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(str, contents); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 5, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 6, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Screen: resize more cols requiring a wide spacer head" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 2, 2, 0); defer s.deinit(); const str = "xx😀"; try s.testWriteString(str); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("xx\n😀", contents); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }
// This resizes to 3 columns, which isn't enough space for our wide // char to enter row 1. But we need to mark the wide spacer head on the // end of the first row since we're wrapping to the next row. try s.resize(3, 2); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("xx\n😀", contents); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.wide, cell.wide); try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint); } { const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); }}
test "Screen: select untracked" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 10, 0); defer s.deinit(); try s.testWriteString("ABC DEF\n 123\n456");
try testing.expect(s.selection == null); const tracked = s.pages.countTrackedPins(); try s.select(Selection.init( s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, false, )); try testing.expectEqual(tracked + 2, s.pages.countTrackedPins()); try s.select(null); try testing.expectEqual(tracked, s.pages.countTrackedPins());}
test "Screen: selectAll" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 10, 0); defer s.deinit();
{ try s.testWriteString("ABC DEF\n 123\n456"); var sel = s.selectAll().?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 2, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
{ try s.testWriteString("\nFOO\n BAR\n BAZ\n QWERTY\n 12345678"); var sel = s.selectAll().?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 8, .y = 7, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectLine" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 10, 0); defer s.deinit(); try s.testWriteString("ABC DEF\n 123\n456");
// Outside of active area // try testing.expect(s.selectLine(.{ .x = 13, .y = 0 }) == null); // try testing.expect(s.selectLine(.{ .x = 0, .y = 5 }) == null);
// Going forward { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 0, .y = 0, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 7, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going backward { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 7, .y = 0, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 7, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going forward and backward { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 3, .y = 0, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 7, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Outside active area { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 9, .y = 0, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 7, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectLine across soft-wrap" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 10, 0); defer s.deinit(); try s.testWriteString(" 12 34012 \n 123");
// Going forward { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 1, .y = 0, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectLine across full soft-wrap" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0); defer s.deinit(); try s.testWriteString("1ABCD2EFGH\n3IJKL");
{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 2, .y = 1, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 4, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectLine across soft-wrap ignores blank lines" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 10, 0); defer s.deinit(); try s.testWriteString(" 12 34012 \n 123");
// Going forward { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 1, .y = 0, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going backward { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 1, .y = 1, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going forward and backward { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 3, .y = 0, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectLine disabled whitespace trimming" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 10, 0); defer s.deinit(); try s.testWriteString(" 12 34012 \n 123");
// Going forward { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 1, .y = 0, } }).?, .whitespace = null, }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 4, .y = 2, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Non-wrapped { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 1, .y = 3, } }).?, .whitespace = null, }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 3, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 4, .y = 3, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectLine with scrollback" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 2, 3, 5); defer s.deinit(); try s.testWriteString("1A\n2B\n3C\n4D\n5E");
// Selecting first line { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 0, .y = 0, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.active, sel.end()).?); }
// Selecting last line { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 0, .y = 2, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 2, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 2, } }, s.pages.pointFromPin(.active, sel.end()).?); }}
// https://github.com/mitchellh/ghostty/issues/1329test "Screen: selectLine semantic prompt boundary" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 10, 0); defer s.deinit(); try s.testWriteSemanticString("ABCDE\n", .unknown); try s.testWriteSemanticString("A ", .prompt); try s.testWriteSemanticString("> ", .unknown);
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings("ABCDE\nA \n> ", contents); }
// Selecting output stops at the prompt even if soft-wrapped { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 1, .y = 1, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 1, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 1, } }, s.pages.pointFromPin(.active, sel.end()).?); } { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ .x = 1, .y = 2, } }).? }).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 2, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 2, } }, s.pages.pointFromPin(.active, sel.end()).?); }}
test "Screen: selectWord" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 10, 0); defer s.deinit(); try s.testWriteString("ABC DEF\n 123\n456");
// Outside of active area // try testing.expect(s.selectWord(.{ .x = 9, .y = 0 }) == null); // try testing.expect(s.selectWord(.{ .x = 0, .y = 5 }) == null);
// Going forward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 0, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going backward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going forward and backward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Whitespace { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 4, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Whitespace single char { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 0, .y = 1, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 1, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// End of screen { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 2, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 2, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectWord across soft-wrap" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 10, 0); defer s.deinit(); try s.testWriteString(" 1234012\n 123");
{ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); try testing.expectEqualStrings(" 1234\n012\n 123", contents); }
// Going forward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going backward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going forward and backward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectWord whitespace across soft-wrap" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 10, 0); defer s.deinit(); try s.testWriteString("1 1\n 123");
// Going forward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going backward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Going forward and backward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectWord with character boundary" { const testing = std.testing; const alloc = testing.allocator;
const cases = [_][]const u8{ " 'abc' \n123", " \"abc\" \n123", " │abc│ \n123", " `abc` \n123", " |abc| \n123", " :abc: \n123", " ,abc, \n123", " (abc( \n123", " )abc) \n123", " [abc[ \n123", " ]abc] \n123", " {abc{ \n123", " }abc} \n123", " <abc< \n123", " >abc> \n123", " $abc$ \n123", };
for (cases) |case| { var s = try init(alloc, 20, 10, 0); defer s.deinit(); try s.testWriteString(case);
// Inside character forward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 4, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Inside character backward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 4, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 4, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Inside character bidirectional { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 4, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// On quote // NOTE: this behavior is not ideal, so we can change this one day, // but I think its also not that important compared to the above. { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, .y = 0, } }, s.pages.pointFromPin(.screen, sel.end()).?); } }}
test "Screen: selectOutput" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 15, 0); defer s.deinit();
// zig fmt: off { // line number: try s.testWriteSemanticString("output1\n", .command); // 0 try s.testWriteSemanticString("output1\n", .command); // 1 try s.testWriteSemanticString("prompt2\n", .prompt); // 2 try s.testWriteSemanticString("input2\n", .input); // 3 try s.testWriteSemanticString( // "output2output2output2output2\n", // 4, 5, 6 due to overflow .command, // ); // try s.testWriteSemanticString("output2\n", .command); // 7 try s.testWriteSemanticString("$ ", .prompt); // 8 prompt try s.testWriteSemanticString("input3\n", .input); // 8 input try s.testWriteSemanticString("output3\n", .command); // 9 try s.testWriteSemanticString("output3\n", .command); // 10 try s.testWriteSemanticString("output3", .command); // 11 } // zig fmt: on
// No start marker, should select from the beginning { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 9, .y = 1, } }, s.pages.pointFromPin(.active, sel.end()).?); } // Both start and end markers, should select between them { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 3, .y = 7, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 4, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 9, .y = 7, } }, s.pages.pointFromPin(.active, sel.end()).?); } // No end marker, should select till the end { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 2, .y = 10, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, .y = 9, } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 9, .y = 11, } }, s.pages.pointFromPin(.active, sel.end()).?); } // input / prompt at y = 0, pt.y = 0 { s.deinit(); s = try init(alloc, 10, 5, 0); try s.testWriteSemanticString("$ ", .prompt); try s.testWriteSemanticString("input1\n", .input); try s.testWriteSemanticString("output1\n", .command); try s.testWriteSemanticString("prompt2\n", .prompt); try testing.expect(s.selectOutput(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, } }).?) == null); }}
test "Screen: selectPrompt basics" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 15, 0); defer s.deinit();
// zig fmt: off { // line number: try s.testWriteSemanticString("output1\n", .command); // 0 try s.testWriteSemanticString("output1\n", .command); // 1 try s.testWriteSemanticString("prompt2\n", .prompt); // 2 try s.testWriteSemanticString("input2\n", .input); // 3 try s.testWriteSemanticString("output2\n", .command); // 4 try s.testWriteSemanticString("output2\n", .command); // 5 try s.testWriteSemanticString("$ ", .prompt); // 6 prompt try s.testWriteSemanticString("input3\n", .input); // 6 input try s.testWriteSemanticString("output3\n", .command); // 7 try s.testWriteSemanticString("output3\n", .command); // 8 try s.testWriteSemanticString("output3", .command); // 9 } // zig fmt: on
// Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 0, .y = 1, } }).?); try testing.expect(sel == null); } { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 0, .y = 8, } }).?); try testing.expect(sel == null); }
// Single line prompt { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 6, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 6, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 9, .y = 6, } }, s.pages.pointFromPin(.screen, sel.end()).?); }
// Multi line prompt { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 3, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 9, .y = 3, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectPrompt prompt at start" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 15, 0); defer s.deinit();
// zig fmt: off { // line number: try s.testWriteSemanticString("prompt1\n", .prompt); // 0 try s.testWriteSemanticString("input1\n", .input); // 1 try s.testWriteSemanticString("output2\n", .command); // 2 try s.testWriteSemanticString("output2\n", .command); // 3 } // zig fmt: on
// Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 0, .y = 3, } }).?); try testing.expect(sel == null); }
// Multi line prompt { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 9, .y = 1, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: selectPrompt prompt at end" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 15, 0); defer s.deinit();
// zig fmt: off { // line number: try s.testWriteSemanticString("output2\n", .command); // 0 try s.testWriteSemanticString("output2\n", .command); // 1 try s.testWriteSemanticString("prompt1\n", .prompt); // 2 try s.testWriteSemanticString("input1\n", .input); // 3 } // zig fmt: on
// Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 0, .y = 1, } }).?); try testing.expect(sel == null); }
// Multi line prompt { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 2, } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, } }, s.pages.pointFromPin(.screen, sel.start()).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 9, .y = 3, } }, s.pages.pointFromPin(.screen, sel.end()).?); }}
test "Screen: promptPath" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 15, 0); defer s.deinit();
// zig fmt: off { // line number: try s.testWriteSemanticString("output1\n", .command); // 0 try s.testWriteSemanticString("output1\n", .command); // 1 try s.testWriteSemanticString("prompt2\n", .prompt); // 2 try s.testWriteSemanticString("input2\n", .input); // 3 try s.testWriteSemanticString("output2\n", .command); // 4 try s.testWriteSemanticString("output2\n", .command); // 5 try s.testWriteSemanticString("$ ", .prompt); // 6 prompt try s.testWriteSemanticString("input3\n", .input); // 6 input try s.testWriteSemanticString("output3\n", .command); // 7 try s.testWriteSemanticString("output3\n", .command); // 8 try s.testWriteSemanticString("output3", .command); // 9 } // zig fmt: on
// From is not in the prompt { const path = s.promptPath( s.pages.pin(.{ .active = .{ .x = 0, .y = 1 } }).?, s.pages.pin(.{ .active = .{ .x = 0, .y = 2 } }).?, ); try testing.expectEqual(@as(isize, 0), path.x); try testing.expectEqual(@as(isize, 0), path.y); }
// Same line { const path = s.promptPath( s.pages.pin(.{ .active = .{ .x = 6, .y = 2 } }).?, s.pages.pin(.{ .active = .{ .x = 3, .y = 2 } }).?, ); try testing.expectEqual(@as(isize, -3), path.x); try testing.expectEqual(@as(isize, 0), path.y); }
// Different lines { const path = s.promptPath( s.pages.pin(.{ .active = .{ .x = 6, .y = 2 } }).?, s.pages.pin(.{ .active = .{ .x = 3, .y = 3 } }).?, ); try testing.expectEqual(@as(isize, -3), path.x); try testing.expectEqual(@as(isize, 1), path.y); }
// To is out of bounds before { const path = s.promptPath( s.pages.pin(.{ .active = .{ .x = 6, .y = 2 } }).?, s.pages.pin(.{ .active = .{ .x = 3, .y = 1 } }).?, ); try testing.expectEqual(@as(isize, -6), path.x); try testing.expectEqual(@as(isize, 0), path.y); }
// To is out of bounds after { const path = s.promptPath( s.pages.pin(.{ .active = .{ .x = 6, .y = 2 } }).?, s.pages.pin(.{ .active = .{ .x = 3, .y = 9 } }).?, ); try testing.expectEqual(@as(isize, 3), path.x); try testing.expectEqual(@as(isize, 1), path.y); }}
test "Screen: selectionString basic" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str);
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 1 } }).?, s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "2EFGH\n3IJ"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString start outside of written area" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 10, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str);
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 5 } }).?, s.pages.pin(.{ .screen = .{ .x = 2, .y = 6 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = ""; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString end outside of written area" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 10, 0); defer s.deinit(); const str = "1ABCD\n2EFGH\n3IJKL"; try s.testWriteString(str);
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 2 } }).?, s.pages.pin(.{ .screen = .{ .x = 2, .y = 6 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "3IJKL"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString trim space" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1AB \n2EFGH\n3IJKL"; try s.testWriteString(str);
const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?, false, );
{ const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "1AB\n2EF"; try testing.expectEqualStrings(expected, contents); }
// No trim { const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = false, }); defer alloc.free(contents); const expected = "1AB \n2EF"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString trim empty line" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0); defer s.deinit(); const str = "1AB \n\n2EFGH\n3IJKL"; try s.testWriteString(str);
const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?, false, );
{ const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "1AB\n\n2EF"; try testing.expectEqualStrings(expected, contents); }
// No trim { const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = false, }); defer alloc.free(contents); const expected = "1AB \n \n2EF"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString soft wrap" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABCD2EFGH3IJKL"; try s.testWriteString(str);
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 1 } }).?, s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "2EFGH3IJ"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString wide char" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1A⚡"; try s.testWriteString(str);
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = str; try testing.expectEqualStrings(expected, contents); }
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = str; try testing.expectEqualStrings(expected, contents); }
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "⚡"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString wide char with header" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0); defer s.deinit(); const str = "1ABC⚡"; try s.testWriteString(str);
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 4, .y = 0 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = str; try testing.expectEqualStrings(expected, contents); }}
// https://github.com/mitchellh/ghostty/issues/289test "Screen: selectionString empty with soft wrap" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 2, 0); defer s.deinit();
// Let me describe the situation that caused this because this // test is not obvious. By writing an emoji below, we introduce // one cell with the emoji and one cell as a "wide char spacer". // We then soft wrap the line by writing spaces. // // By selecting only the tail, we'd select nothing and we had // a logic error that would cause a crash. try s.testWriteString("👨"); try s.testWriteString(" ");
{ const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "👨"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString with zero width joiner" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 1, 0); defer s.deinit(); const str = "👨"; // this has a ZWJ try s.testWriteString(str);
// Integrity check { const pin = s.pages.pin(.{ .screen = .{ .y = 0, .x = 0 } }).?; const cell = pin.rowAndCell().cell; try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint); try testing.expectEqual(Cell.Wide.wide, cell.wide); const cps = pin.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); }
// The real test { const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "👨"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: selectionString, rectangle, basic" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 30, 5, 0); defer s.deinit(); const str = \\Lorem ipsum dolor \\sit amet, consectetur \\adipiscing elit, sed do \\eiusmod tempor incididunt \\ut labore et dolore ; const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?, s.pages.pin(.{ .screen = .{ .x = 6, .y = 3 } }).?, true, ); const expected = \\t ame \\ipisc \\usmod ; try s.testWriteString(str);
const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); try testing.expectEqualStrings(expected, contents);}
test "Screen: selectionString, rectangle, w/EOL" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 30, 5, 0); defer s.deinit(); const str = \\Lorem ipsum dolor \\sit amet, consectetur \\adipiscing elit, sed do \\eiusmod tempor incididunt \\ut labore et dolore ; const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 12, .y = 0 } }).?, s.pages.pin(.{ .screen = .{ .x = 26, .y = 4 } }).?, true, ); const expected = \\dolor \\nsectetur \\lit, sed do \\or incididunt \\ dolore ; try s.testWriteString(str);
const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); try testing.expectEqualStrings(expected, contents);}
test "Screen: selectionString, rectangle, more complex w/breaks" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 30, 8, 0); defer s.deinit(); const str = \\Lorem ipsum dolor \\sit amet, consectetur \\adipiscing elit, sed do \\eiusmod tempor incididunt \\ut labore et dolore \\ \\magna aliqua. Ut enim \\ad minim veniam, quis ; const sel = Selection.init( s.pages.pin(.{ .screen = .{ .x = 11, .y = 2 } }).?, s.pages.pin(.{ .screen = .{ .x = 26, .y = 7 } }).?, true, ); const expected = \\elit, sed do \\por incididunt \\t dolore \\ \\a. Ut enim \\niam, quis ; try s.testWriteString(str);
const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); try testing.expectEqualStrings(expected, contents);}
test "Screen: selectionString multi-page" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 10, 3, 2048); defer s.deinit();
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
// Lazy way to seek to the first page boundary. s.pages.pages.first.?.data.pauseIntegrityChecks(true); for (0..first_page_size - 1) |_| { try s.testWriteString("\n"); } s.pages.pages.first.?.data.pauseIntegrityChecks(false);
try s.testWriteString("123456789\n!@#$%^&*(\n123456789");
{ const sel = Selection.init( s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, s.pages.pin(.{ .active = .{ .x = 2, .y = 2 } }).?, false, ); const contents = try s.selectionString(alloc, .{ .sel = sel, .trim = true, }); defer alloc.free(contents); const expected = "123456789\n!@#$%^&*(\n123"; try testing.expectEqualStrings(expected, contents); }}
test "Screen: lineIterator" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0); defer s.deinit(); const str = "1ABCD\n2EFGH"; try s.testWriteString(str);
// Test the line iterator var iter = s.lineIterator(s.pages.pin(.{ .viewport = .{} }).?); { const sel = iter.next().?; const actual = try s.selectionString(alloc, .{ .sel = sel, .trim = false, }); defer alloc.free(actual); try testing.expectEqualStrings("1ABCD", actual); } { const sel = iter.next().?; const actual = try s.selectionString(alloc, .{ .sel = sel, .trim = false, }); defer alloc.free(actual); try testing.expectEqualStrings("2EFGH", actual); }}
test "Screen: lineIterator soft wrap" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0); defer s.deinit(); const str = "1ABCD2EFGH\n3ABCD"; try s.testWriteString(str);
// Test the line iterator var iter = s.lineIterator(s.pages.pin(.{ .viewport = .{} }).?); { const sel = iter.next().?; const actual = try s.selectionString(alloc, .{ .sel = sel, .trim = false, }); defer alloc.free(actual); try testing.expectEqualStrings("1ABCD2EFGH", actual); } { const sel = iter.next().?; const actual = try s.selectionString(alloc, .{ .sel = sel, .trim = false, }); defer alloc.free(actual); try testing.expectEqualStrings("3ABCD", actual); } // try testing.expect(iter.next() == null);}
test "Screen: hyperlink start/end" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0); defer s.deinit(); try testing.expect(s.cursor.hyperlink_id == 0); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); }
try s.startHyperlink("http://example.com", null); try testing.expect(s.cursor.hyperlink_id != 0); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); }
s.endHyperlink(); try testing.expect(s.cursor.hyperlink_id == 0); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); }}
test "Screen: hyperlink reuse" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0); defer s.deinit();
try testing.expect(s.cursor.hyperlink_id == 0); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); }
// Use it for the first time try s.startHyperlink("http://example.com", null); try testing.expect(s.cursor.hyperlink_id != 0); const id = s.cursor.hyperlink_id;
// Reuse the same hyperlink, expect we have the same ID try s.startHyperlink("http://example.com", null); try testing.expectEqual(id, s.cursor.hyperlink_id); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); }
s.endHyperlink(); try testing.expect(s.cursor.hyperlink_id == 0); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); }}
test "Screen: hyperlink cursor state on resize" { const testing = std.testing; const alloc = testing.allocator;
// This test depends on underlying PageList implementation so // it may be invalid one day. It's here to document/verify the // current behavior.
var s = try init(alloc, 5, 10, 0); defer s.deinit();
// Start a hyperlink try s.startHyperlink("http://example.com", null); try testing.expect(s.cursor.hyperlink_id != 0); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); }
// Resize. Any column growth will trigger a page to be reallocated. try s.resize(10, 10); try testing.expect(s.cursor.hyperlink_id != 0); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(1, page.hyperlink_set.count()); }
s.endHyperlink(); try testing.expect(s.cursor.hyperlink_id == 0); { const page = &s.cursor.page_pin.node.data; try testing.expectEqual(0, page.hyperlink_set.count()); }}
test "Screen: cursorSetHyperlink OOM + URI too large for string alloc" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 80, 24, 0); defer s.deinit();
// Start a hyperlink with a URI that just barely fits in the string alloc. // This will ensure that additional string alloc space is needed for the // redundant copy of the URI when the page is re-alloced. const uri = "a" ** (pagepkg.std_capacity.string_bytes - 8); try s.startHyperlink(uri, null);
// Figure out how many cells should can have hyperlinks in this page, // and write twice that number, to guarantee the capacity needs to be // increased at some point. const base_capacity = s.cursor.page_pin.node.data.hyperlinkCapacity(); const base_string_bytes = s.cursor.page_pin.node.data.capacity.string_bytes; for (0..base_capacity * 2) |_| { try s.cursorSetHyperlink(); if (s.cursor.x >= s.pages.cols - 1) { try s.cursorDownOrScroll(); s.cursorHorizontalAbsolute(0); } else { s.cursorRight(1); } }
// Make sure the capacity really did increase. try testing.expect(base_capacity < s.cursor.page_pin.node.data.hyperlinkCapacity()); // And that our string_bytes increased as well. try testing.expect(base_string_bytes < s.cursor.page_pin.node.data.capacity.string_bytes);}
test "Screen: adjustCapacity cursor style ref count" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 5, 5, 0); defer s.deinit();
try s.setAttribute(.{ .bold = {} }); try s.testWriteString("1ABCD");
{ const page = &s.pages.pages.last.?.data; try testing.expectEqual( 6, // All chars + cursor page.styles.refCount(page.memory, s.cursor.style_id), ); }
// This forces the page to change. _ = try s.adjustCapacity( s.cursor.page_pin.node, .{ .grapheme_bytes = s.cursor.page_pin.node.data.capacity.grapheme_bytes * 2 }, );
// Our ref counts should still be the same { const page = &s.pages.pages.last.?.data; try testing.expectEqual( 6, // All chars + cursor page.styles.refCount(page.memory, s.cursor.style_id), ); }}
test "Screen: adjustCapacity cursor hyperlink exceeds string alloc size" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 80, 24, 0); defer s.deinit();
// Start a hyperlink with a URI that just barely fits in the string alloc. // This will ensure that the redundant copy added in `adjustCapacity` won't // fit in the available string alloc space. const uri = "a" ** (pagepkg.std_capacity.string_bytes - 8); try s.startHyperlink(uri, null);
// Write some characters with this so that the URI // is copied to the new page when adjusting capacity. try s.testWriteString("Hello");
// Adjust the capacity, right now this will cause a redundant copy of // the URI to be added to the string alloc, but since there isn't room // for this this will clear the cursor hyperlink. _ = try s.adjustCapacity(s.cursor.page_pin.node, .{});
// The cursor hyperlink should have been cleared by the `adjustCapacity` // call, because there isn't enough room to add the redundant URI string. // // This behavior will change, causing this test to fail, if any of these // changes are made: // // - The string alloc is changed to intern strings. // // - The adjustCapacity function is changed to ensure the new // capacity will fit the redundant copy of the hyperlink uri. // // - The cursor managed memory handling is reworked so that it // doesn't reside in the pages anymore and doesn't need this // accounting. // // In such a case, adjust this test accordingly. try testing.expectEqual(null, s.cursor.hyperlink); try testing.expectEqual(0, s.cursor.hyperlink_id);}
test "Screen: adjustCapacity cursor style exceeds style set capacity" { const testing = std.testing; const alloc = testing.allocator;
var s = try init(alloc, 80, 24, 1000); defer s.deinit();
const page = &s.cursor.page_pin.node.data;
// We add unique styles to the page until no more will fit. fill: for (0..255) |bg| { for (0..255) |fg| { const st: style.Style = .{ .bg_color = .{ .palette = @intCast(bg) }, .fg_color = .{ .palette = @intCast(fg) }, };
s.cursor.style = st;
// Try to insert the new style, if it doesn't fit then // we succeeded in filling the style set, so we break. s.cursor.style_id = page.styles.add( page.memory, s.cursor.style, ) catch break :fill;
try s.testWriteString("a"); } }
// Adjust the capacity, this should cause the style set to reach the // same state it was in to begin with, since it will clone the page // in the same order as the styles were added to begin with, meaning // the cursor style will not be able to be added to the set, which // should, right now, result in the cursor style being cleared. _ = try s.adjustCapacity(s.cursor.page_pin.node, .{});
// The cursor style should have been cleared by the `adjustCapacity`. // // This behavior will change, causing this test to fail, if either // of these changes are made: // // - The adjustCapacity function is changed to ensure the // new capacity will definitely fit the cursor style. // // - The cursor managed memory handling is reworked so that it // doesn't reside in the pages anymore and doesn't need this // accounting. // // In such a case, adjust this test accordingly. try testing.expect(s.cursor.style.default()); try testing.expectEqual(style.default_id, s.cursor.style_id);}
test "Screen UTF8 cell map with newlines" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); try s.testWriteString("A\n\nB\n\nC");
var cell_map = Page.CellMap.init(alloc); defer cell_map.deinit(); var builder = std.ArrayList(u8).init(alloc); defer builder.deinit(); try s.dumpString(builder.writer(), .{ .tl = s.pages.getTopLeft(.screen), .br = s.pages.getBottomRight(.screen), .cell_map = &cell_map, });
try testing.expectEqual(7, builder.items.len); try testing.expectEqualStrings("A\n\nB\n\nC", builder.items); try testing.expectEqual(builder.items.len, cell_map.items.len); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 0, }, cell_map.items[0]); try testing.expectEqual(Page.CellMapEntry{ .x = 1, .y = 0, }, cell_map.items[1]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 1, }, cell_map.items[2]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 2, }, cell_map.items[3]);}
test "Screen UTF8 cell map with blank prefix" { const testing = std.testing; const alloc = testing.allocator;
var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); s.cursorAbsolute(2, 1); try s.testWriteString("B");
var cell_map = Page.CellMap.init(alloc); defer cell_map.deinit(); var builder = std.ArrayList(u8).init(alloc); defer builder.deinit(); try s.dumpString(builder.writer(), .{ .tl = s.pages.getTopLeft(.screen), .br = s.pages.getBottomRight(.screen), .cell_map = &cell_map, });
try testing.expectEqualStrings("\n B", builder.items); try testing.expectEqual(builder.items.len, cell_map.items.len); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 0, }, cell_map.items[0]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 1, }, cell_map.items[1]); try testing.expectEqual(Page.CellMapEntry{ .x = 1, .y = 1, }, cell_map.items[2]); try testing.expectEqual(Page.CellMapEntry{ .x = 2, .y = 1, }, cell_map.items[3]);}