Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006900790089009901090119012901390149015901690179018901990209021902290239024902590269027902890299030903190329033903490359036903790389039904090419042904390449045904690479048904990509051905290539054905590569057905890599060906190629063906490659066906790689069907090719072907390749075907690779078907990809081908290839084908590869087908890899090909190929093909490959096909790989099910091019102910391049105910691079108910991109111911291139114911591169117911891199120912191229123912491259126912791289129913091319132913391349135913691379138913991409141914291439144914591469147914891499150915191529153915491559156915791589159916091619162916391649165916691679168916991709171917291739174917591769177917891799180918191829183918491859186918791889189919091919192919391949195919691979198919992009201920292039204920592069207920892099210921192129213921492159216921792189219922092219222922392249225922692279228922992309231923292339234923592369237923892399240924192429243924492459246924792489249925092519252925392549255925692579258925992609261926292639264926592669267926892699270927192729273927492759276927792789279928092819282928392849285928692879288928992909291929292939294929592969297929892999300930193029303930493059306930793089309931093119312931393149315931693179318931993209321932293239324932593269327932893299330933193329333933493359336933793389339934093419342934393449345934693479348934993509351935293539354935593569357935893599360936193629363936493659366936793689369937093719372937393749375937693779378937993809381938293839384938593869387938893899390939193929393939493959396939793989399940094019402940394049405940694079408940994109411941294139414941594169417941894199420942194229423942494259426942794289429943094319432943394349435943694379438943994409441944294439444944594469447944894499450945194529453945494559456945794589459946094619462946394649465946694679468946994709471947294739474947594769477947894799480948194829483948494859486948794889489949094919492949394949495949694979498949995009501950295039504950595069507950895099510951195129513951495159516951795189519952095219522952395249525952695279528952995309531953295339534953595369537953895399540954195429543954495459546954795489549955095519552955395549555955695579558955995609561956295639564956595669567956895699570957195729573957495759576957795789579958095819582958395849585958695879588958995909591959295939594959595969597959895999600960196029603960496059606960796089609961096119612961396149615961696179618961996209621962296239624962596269627962896299630963196329633963496359636963796389639964096419642964396449645964696479648964996509651965296539654965596569657965896599660966196629663966496659666966796689669967096719672967396749675967696779678967996809681968296839684968596869687968896899690969196929693969496959696969796989699970097019702970397049705970697079708970997109711971297139714971597169717971897199720972197229723972497259726972797289729973097319732973397349735973697379738973997409741974297439744974597469747974897499750975197529753975497559756975797589759976097619762976397649765976697679768976997709771977297739774977597769777977897799780978197829783978497859786978797889789979097919792979397949795979697979798979998009801980298039804980598069807980898099810981198129813981498159816981798189819982098219822982398249825982698279828982998309831983298339834983598369837983898399840984198429843984498459846984798489849985098519852985398549855985698579858985998609861986298639864986598669867986898699870987198729873987498759876987798789879988098819882988398849885988698879888988998909891989298939894989598969897989898999900990199029903990499059906990799089909991099119912991399149915991699179918991999209921992299239924992599269927992899299930993199329933993499359936993799389939994099419942994399449945994699479948994999509951995299539954995599569957995899599960996199629963996499659966996799689969997099719972997399749975997699779978997999809981998299839984998599869987998899899990999199929993999499959996999799989999100001000110002100031000410005100061000710008100091001010011100121001310014100151001610017100181001910020100211002210023100241002510026100271002810029100301003110032100331003410035100361003710038100391004010041100421004310044100451004610047100481004910050100511005210053100541005510056100571005810059100601006110062100631006410065100661006710068100691007010071100721007310074100751007610077100781007910080100811008210083100841008510086100871008810089100901009110092100931009410095100961009710098100991010010101101021010310104101051010610107101081010910110101111011210113101141011510116101171011810119101201012110122101231012410125101261012710128101291013010131101321013310134101351013610137101381013910140101411014210143101441014510146101471014810149101501015110152101531015410155101561015710158101591016010161101621016310164101651016610167101681016910170101711017210173101741017510176101771017810179101801018110182101831018410185101861018710188101891019010191101921019310194101951019610197101981019910200102011020210203102041020510206102071020810209102101021110212102131021410215102161021710218102191022010221102221022310224102251022610227102281022910230102311023210233102341023510236102371023810239102401024110242102431024410245102461024710248102491025010251102521025310254102551025610257102581025910260102611026210263102641026510266102671026810269102701027110272102731027410275102761027710278102791028010281102821028310284102851028610287102881028910290102911029210293102941029510296102971029810299103001030110302103031030410305103061030710308103091031010311103121031310314103151031610317103181031910320103211032210323103241032510326103271032810329103301033110332103331033410335103361033710338103391034010341103421034310344103451034610347103481034910350103511035210353103541035510356103571035810359103601036110362103631036410365103661036710368103691037010371103721037310374103751037610377103781037910380103811038210383103841038510386103871038810389103901039110392103931039410395103961039710398103991040010401104021040310404104051040610407104081040910410104111041210413104141041510416104171041810419104201042110422104231042410425104261042710428104291043010431104321043310434104351043610437104381043910440104411044210443104441044510446104471044810449104501045110452104531045410455104561045710458104591046010461104621046310464104651046610467104681046910470104711047210473104741047510476104771047810479104801048110482104831048410485104861048710488104891049010491104921049310494104951049610497104981049910500105011050210503105041050510506105071050810509105101051110512105131051410515105161051710518105191052010521105221052310524105251052610527105281052910530105311053210533105341053510536105371053810539105401054110542105431054410545105461054710548105491055010551105521055310554105551055610557105581055910560105611056210563105641056510566105671056810569105701057110572105731057410575105761057710578105791058010581105821058310584105851058610587105881058910590105911059210593105941059510596105971059810599106001060110602106031060410605106061060710608106091061010611106121061310614106151061610617106181061910620106211062210623106241062510626106271062810629106301063110632106331063410635106361063710638106391064010641106421064310644106451064610647106481064910650106511065210653106541065510656106571065810659106601066110662106631066410665106661066710668106691067010671106721067310674106751067610677106781067910680106811068210683106841068510686106871068810689106901069110692106931069410695106961069710698106991070010701107021070310704107051070610707107081070910710107111071210713107141071510716107171071810719107201072110722107231072410725107261072710728107291073010731107321073310734107351073610737107381073910740107411074210743107441074510746107471074810749107501075110752107531075410755107561075710758107591076010761107621076310764107651076610767107681076910770107711077210773107741077510776107771077810779107801078110782107831078410785107861078710788107891079010791107921079310794107951079610797107981079910800108011080210803108041080510806108071080810809108101081110812108131081410815108161081710818108191082010821108221082310824108251082610827108281082910830108311083210833108341083510836108371083810839108401084110842108431084410845108461084710848108491085010851108521085310854108551085610857108581085910860108611086210863108641086510866108671086810869108701087110872108731087410875108761087710878108791088010881108821088310884108851088610887108881088910890108911089210893108941089510896108971089810899109001090110902109031090410905109061090710908109091091010911109121091310914109151091610917109181091910920109211092210923109241092510926109271092810929109301093110932109331093410935/// Config is the main config struct. These fields map directly to the/// CLI flag names hence we use a lot of `@""` syntax to support hyphens.
// Pandoc is used to automatically generate manual pages and other forms of// documentation, so documentation comments on fields in the Config struct// should use Pandoc's flavor of Markdown.//// For a reference to Pandoc's Markdown see their [online// manual.](https://pandoc.org/MANUAL.html#pandocs-markdown)
const Config = @This();
const std = @import("std");const builtin = @import("builtin");const build_config = @import("../build_config.zig");const assert = @import("../quirks.zig").inlineAssert;const Allocator = std.mem.Allocator;const ArenaAllocator = std.heap.ArenaAllocator;const global_state = &@import("../global.zig").state;const deepEqual = @import("../datastruct/comparison.zig").deepEqual;const fontpkg = @import("../font/main.zig");const inputpkg = @import("../input.zig");const internal_os = @import("../os/main.zig");const cli = @import("../cli.zig");
const conditional = @import("conditional.zig");const Conditional = conditional.Conditional;const file_load = @import("file_load.zig");const formatterpkg = @import("formatter.zig");const themepkg = @import("theme.zig");const url = @import("url.zig");pub const Key = @import("key.zig").Key;const MetricModifier = fontpkg.Metrics.Modifier;const help_strings = @import("help_strings");pub const Command = @import("command.zig").Command;const RepeatableReadableIO = @import("io.zig").RepeatableReadableIO;const RepeatableStringMap = @import("RepeatableStringMap.zig");pub const Path = @import("path.zig").Path;pub const RepeatablePath = @import("path.zig").RepeatablePath;const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig");const KeyRemapSet = @import("../input/key_mods.zig").RemapSet;pub const WindowPaddingBalance = @import("../renderer/size.zig").PaddingBalance;const string = @import("string.zig");
// We do this instead of importing all of terminal/main.zig to// limit the dependency graph. This is important because some things// like the `ghostty-build-data` binary depend on the Config but don't// want to include all the other stuff.const terminal = struct { const CursorStyle = @import("../terminal/cursor.zig").Style; const color = @import("../terminal/color.zig"); const selection_codepoints = @import("../terminal/selection_codepoints.zig"); const style = @import("../terminal/style.zig"); const x11_color = @import("../terminal/x11_color.zig");};
const log = std.log.scoped(.config);
/// Used on Unixes for some defaults.const c = @cImport({ @cInclude("unistd.h");});
pub const compatibility = std.StaticStringMap( cli.CompatibilityHandler(Config),).initComptime(&.{ // Ghostty 1.1 introduced background-blur support for Linux which // doesn't support a specific radius value. The renaming is to let // one field be used for both platforms (macOS retained the ability // to set a radius). .{ "background-blur-radius", cli.compatibilityRenamed(Config, "background-blur") },
// Ghostty 1.2 renamed all our adw options to gtk because we now have // a hard dependency on libadwaita. .{ "adw-toolbar-style", cli.compatibilityRenamed(Config, "gtk-toolbar-style") },
// Ghostty 1.2 removed the `hidden` value from `gtk-tabs-location` and // moved it to `window-show-tab-bar`. .{ "gtk-tabs-location", compatGtkTabsLocation },
// Ghostty 1.2 lets you set `cell-foreground` and `cell-background` // to match the cell foreground and background colors, respectively. // This can be used with `cursor-color` and `cursor-text` to recreate // this behavior. This applies to selection too. .{ "cursor-invert-fg-bg", compatCursorInvertFgBg }, .{ "selection-invert-fg-bg", compatSelectionInvertFgBg },
// Ghostty 1.2 merged `bold-is-bright` into the new `bold-color` // by setting the value to "bright". .{ "bold-is-bright", compatBoldIsBright },
// Ghostty 1.2 removed the "desktop" option and renamed it to "detect". // The semantics also changed slightly but this is the correct mapping. .{ "gtk-single-instance", compatGtkSingleInstance },
// Ghostty 1.3 rename the "window" option to "new-window". // See: https://github.com/ghostty-org/ghostty/pull/9764 .{ "macos-dock-drop-behavior", compatMacOSDockDropBehavior },});
/// Set Ghostty's graphical user interface language to a language other than the/// system default language. For example:////// language = de////// will force the strings in Ghostty's graphical user interface to be in German/// rather than the system default.////// This will not affect the language used by programs run _within_ Ghostty./// Those will continue to use the default system language. There are also many/// non-GUI elements in Ghostty that are not translated - this setting will have/// no effect on those.////// Warning: This setting cannot be reloaded at runtime. To change the language/// you must fully restart Ghostty.////// GTK only./// Available since 1.3.0.language: ?[:0]const u8 = null,
/// The font families to use.////// You can generate the list of valid values using the CLI:////// ghostty +list-fonts////// This configuration can be repeated multiple times to specify preferred/// fallback fonts when the requested codepoint is not available in the primary/// font. This is particularly useful for multiple languages, symbolic fonts,/// etc.////// Notes on emoji specifically: On macOS, Ghostty by default will always use/// Apple Color Emoji and on Linux will always use Noto Emoji. You can/// override this behavior by specifying a font family here that contains/// emoji glyphs.////// The specific styles (bold, italic, bold italic) do not need to be/// explicitly set. If a style is not set, then the regular style (font-family)/// will be searched for stylistic variants. If a stylistic variant is not/// found, Ghostty will use the regular style. This prevents falling back to a/// different font family just to get a style such as bold. This also applies/// if you explicitly specify a font family for a style. For example, if you/// set `font-family-bold = FooBar` and "FooBar" cannot be found, Ghostty will/// use whatever font is set for `font-family` for the bold style.////// Finally, some styles may be synthesized if they are not supported./// For example, if a font does not have an italic style and no alternative/// italic font is specified, Ghostty will synthesize an italic style by/// applying a slant to the regular style. If you want to disable these/// synthesized styles then you can use the `font-style` configurations/// as documented below.////// You can disable styles completely by using the `font-style` set of/// configurations. See the documentation for `font-style` for more information.////// If you want to overwrite a previous set value rather than append a fallback,/// specify the value as `""` (empty string) to reset the list and then set the/// new values. For example:////// font-family = ""/// font-family = "My Favorite Font"////// Setting any of these as CLI arguments will automatically clear the/// values set in configuration files so you don't need to specify/// `--font-family=""` before setting a new value. You only need to specify/// this within config files if you want to clear previously set values in/// configuration files or on the CLI if you want to clear values set on the/// CLI.@"font-family": RepeatableString = .{},@"font-family-bold": RepeatableString = .{},@"font-family-italic": RepeatableString = .{},@"font-family-bold-italic": RepeatableString = .{},
/// The named font style to use for each of the requested terminal font styles./// This looks up the style based on the font style string advertised by the/// font itself. For example, "Iosevka Heavy" has a style of "Heavy".////// You can also use these fields to completely disable a font style. If you set/// the value of the configuration below to literal `false` then that font style/// will be disabled. If the running program in the terminal requests a disabled/// font style, the regular font style will be used instead.////// These are only valid if its corresponding font-family is also specified. If/// no font-family is specified, then the font-style is ignored unless you're/// disabling the font style.@"font-style": FontStyle = .{ .default = {} },@"font-style-bold": FontStyle = .{ .default = {} },@"font-style-italic": FontStyle = .{ .default = {} },@"font-style-bold-italic": FontStyle = .{ .default = {} },
/// Control whether Ghostty should synthesize a style if the requested style is/// not available in the specified font-family.////// Ghostty can synthesize bold, italic, and bold italic styles if the font/// does not have a specific style. For bold, this is done by drawing an/// outline around the glyph of varying thickness. For italic, this is done by/// applying a slant to the glyph. For bold italic, both of these are applied.////// Synthetic styles are not perfect and will generally not look as good/// as a font that has the style natively. However, they are useful to/// provide styled text when the font does not have the style.////// Set this to "false" or "true" to disable or enable synthetic styles/// completely. You can disable specific styles using "no-bold", "no-italic",/// and "no-bold-italic". You can disable multiple styles by separating them/// with a comma. For example, "no-bold,no-italic".////// Available style keys are: `bold`, `italic`, `bold-italic`.////// If synthetic styles are disabled, then the regular style will be used/// instead if the requested style is not available. If the font has the/// requested style, then the font will be used as-is since the style is/// not synthetic.////// Warning: An easy mistake is to disable `bold` or `italic` but not/// `bold-italic`. Disabling only `bold` or `italic` will NOT disable either/// in the `bold-italic` style. If you want to disable `bold-italic`, you must/// explicitly disable it. You cannot partially disable `bold-italic`.////// By default, synthetic styles are enabled.@"font-synthetic-style": FontSyntheticStyle = .{},
/// Apply a font feature. To enable multiple font features you can repeat/// this multiple times or use a comma-separated list of feature settings.////// The syntax for feature settings is as follows, where `feat` is a feature:////// * Enable features with e.g. `feat`, `+feat`, `feat on`, `feat=1`./// * Disabled features with e.g. `-feat`, `feat off`, `feat=0`./// * Set a feature value with e.g. `feat=2`, `feat = 3`, `feat 4`./// * Feature names may be wrapped in quotes, meaning this config should be/// syntactically compatible with the `font-feature-settings` CSS property.////// The syntax is fairly loose, but invalid settings will be silently ignored.////// The font feature will apply to all fonts rendered by Ghostty. A future/// enhancement will allow targeting specific faces.////// To disable programming ligatures, use `-calt` since this is the typical/// feature name for programming ligatures. To look into what font features/// your font has and what they do, use a font inspection tool such as/// [fontdrop.info](https://fontdrop.info).////// To generally disable most ligatures, use `-calt, -liga, -dlig`.@"font-feature": RepeatableString = .{},
/// Font size in points. This value can be a non-integer and the nearest integer/// pixel size will be selected. If you have a high dpi display where 1pt = 2px/// then you can get an odd numbered pixel size by specifying a half point.////// For example, 13.5pt @ 2px/pt = 27px////// Changing this configuration at runtime will only affect existing/// terminals that have NOT manually adjusted their font size in some way/// (e.g. increased or decreased the font size). Terminals that have manually/// adjusted their font size will retain their manually adjusted size./// Otherwise, the font size of existing terminals will be updated on/// reload.////// On Linux with GTK, font size is scaled according to both display-wide and/// text-specific scaling factors, which are often managed by your desktop/// environment (e.g. the GNOME display scale and large text settings).@"font-size": f32 = switch (builtin.os.tag) { // On macOS we default a little bigger since this tends to look better. This // is purely subjective but this is easy to modify. .macos => 13, else => 12,},
/// A repeatable configuration to set one or more font variations values for/// a variable font. A variable font is a single font, usually with a filename/// ending in `-VF.ttf` or `-VF.otf` that contains one or more configurable axes/// for things such as weight, slant, etc. Not all fonts support variations;/// only fonts that explicitly state they are variable fonts will work.////// The format of this is `id=value` where `id` is the axis identifier. An axis/// identifier is always a 4 character string, such as `wght`. To get the list/// of supported axes, look at your font documentation or use a font inspection/// tool.////// Invalid ids and values are usually ignored. For example, if a font only/// supports weights from 100 to 700, setting `wght=800` will do nothing (it/// will not be clamped to 700). You must consult your font's documentation to/// see what values are supported.////// Common axes are: `wght` (weight), `slnt` (slant), `ital` (italic), `opsz`/// (optical size), `wdth` (width), `GRAD` (gradient), etc.@"font-variation": RepeatableFontVariation = .{},@"font-variation-bold": RepeatableFontVariation = .{},@"font-variation-italic": RepeatableFontVariation = .{},@"font-variation-bold-italic": RepeatableFontVariation = .{},
/// Force one or a range of Unicode codepoints to map to a specific named font./// This is useful if you want to support special symbols or if you want to use/// specific glyphs that render better for your specific font.////// The syntax is `codepoint=fontname` where `codepoint` is either a single/// codepoint or a range. Codepoints must be specified as full Unicode/// hex values, such as `U+ABCD`. Codepoints ranges are specified as/// `U+ABCD-U+DEFG`. You can specify multiple ranges for the same font separated/// by commas, such as `U+ABCD-U+DEFG,U+1234-U+5678=fontname`. The font name is/// the same value as you would use for `font-family`.////// This configuration can be repeated multiple times to specify multiple/// codepoint mappings.////// Changing this configuration at runtime will only affect new terminals,/// i.e. new windows, tabs, etc.@"font-codepoint-map": RepeatableCodepointMap = .{},
/// Map specific Unicode codepoints to replacement values when copying text/// to clipboard.////// This configuration allows you to replace specific Unicode characters with/// other characters or strings when copying terminal content to the clipboard./// This is useful for converting special terminal symbols to more compatible/// characters for pasting into other applications.////// The syntax is similar to `font-codepoint-map`:/// - Single codepoint: `U+1234=U+ABCD` or `U+1234=replacement_text`/// - Codepoint range: `U+1234-U+5678=U+ABCD`////// Examples:/// - `clipboard-codepoint-map = U+2500=U+002D` (box drawing horizontal → hyphen)/// - `clipboard-codepoint-map = U+2502=U+007C` (box drawing vertical → pipe)/// - `clipboard-codepoint-map = U+03A3=SUM` (Greek sigma → "SUM")////// This configuration can be repeated multiple times to specify multiple/// mappings. Later entries take priority over earlier ones for overlapping/// ranges.////// Note: This only applies to text copying operations, not URL copying.@"clipboard-codepoint-map": RepeatableClipboardCodepointMap = .{},
/// Draw fonts with a thicker stroke, if supported./// This is currently only supported on macOS.@"font-thicken": bool = false,
/// Strength of thickening when `font-thicken` is enabled.////// Valid values are integers between `0` and `255`. `0` does not correspond to/// *no* thickening, rather it corresponds to the lightest available thickening.////// Has no effect when `font-thicken` is set to `false`.////// This is currently only supported on macOS.@"font-thicken-strength": u8 = 255,
/// Locations to break font shaping into multiple runs.////// A "run" is a contiguous segment of text that is shaped together. "Shaping"/// is the process of converting text (codepoints) into glyphs (renderable/// characters). This is how ligatures are formed, among other things./// For example, if a coding font turns "!=" into a single glyph, then it/// must see "!" and "=" next to each other in a single run. When a run/// is broken, the text is shaped separately. To continue our example, if/// "!" is at the end of one run and "=" is at the start of the next run,/// then the ligature will not be formed.////// Ghostty breaks runs at certain points to improve readability or usability./// For example, Ghostty by default will break runs under the cursor so that/// text editing can see the individual characters rather than a ligature./// This configuration lets you configure this behavior.////// Combine values with a comma to set multiple options. Prefix an/// option with "no-" to disable it. Enabling and disabling options/// can be done at the same time.////// Available options:////// * `cursor` - Break runs under the cursor.////// Available since: 1.2.0@"font-shaping-break": FontShapingBreak = .{},
/// What color space to use when performing alpha blending.////// This affects the appearance of text and of any images with transparency./// Additionally, custom shaders will receive colors in the configured space.////// On macOS the default is `native`, on all other platforms the default is/// `linear-corrected`.////// Valid values:////// * `native` - Perform alpha blending in the native color space for the OS./// On macOS this corresponds to Display P3, and on Linux it's sRGB.////// * `linear` - Perform alpha blending in linear space. This will eliminate/// the darkening artifacts around the edges of text that are very visible/// when certain color combinations are used (e.g. red / green), but makes/// dark text look much thinner than normal and light text much thicker./// This is also sometimes known as "gamma correction".////// * `linear-corrected` - Same as `linear`, but with a correction step applied/// for text that makes it look nearly or completely identical to `native`,/// but without any of the darkening artifacts.////// Available since: 1.1.0@"alpha-blending": AlphaBlending = if (builtin.os.tag == .macos) .native else .@"linear-corrected",
/// All of the configurations behavior adjust various metrics determined by the/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,/// etc.). In each case, the values represent the amount to change the original/// value.////// For example, a value of `1` increases the value by 1; it does not set it to/// literally 1. A value of `20%` increases the value by 20%. And so on.////// There is little to no validation on these values so the wrong values (e.g./// `-100%`) can cause the terminal to be unusable. Use with caution and reason.////// Some values are clamped to minimum or maximum values. This can make it/// appear that certain values are ignored. For example, many `*-thickness`/// adjustments cannot go below 1px.////// `adjust-cell-height` has some additional behaviors to describe:////// * The font will be centered vertically in the cell.////// * The cursor will remain the same size as the font, but may be/// adjusted separately with `adjust-cursor-height`.////// * Powerline glyphs will be adjusted along with the cell height so/// that things like status lines continue to look aligned.@"adjust-cell-width": ?MetricModifier = null,@"adjust-cell-height": ?MetricModifier = null,/// Distance in pixels or percentage adjustment from the bottom of the cell to the text baseline./// Increase to move baseline UP, decrease to move baseline DOWN./// See the notes about adjustments in `adjust-cell-width`.@"adjust-font-baseline": ?MetricModifier = null,/// Distance in pixels or percentage adjustment from the top of the cell to the top of the underline./// Increase to move underline DOWN, decrease to move underline UP./// See the notes about adjustments in `adjust-cell-width`.@"adjust-underline-position": ?MetricModifier = null,/// Thickness in pixels of the underline./// See the notes about adjustments in `adjust-cell-width`.@"adjust-underline-thickness": ?MetricModifier = null,/// Distance in pixels or percentage adjustment from the top of the cell to the top of the strikethrough./// Increase to move strikethrough DOWN, decrease to move strikethrough UP./// See the notes about adjustments in `adjust-cell-width`.@"adjust-strikethrough-position": ?MetricModifier = null,/// Thickness in pixels or percentage adjustment of the strikethrough./// See the notes about adjustments in `adjust-cell-width`.@"adjust-strikethrough-thickness": ?MetricModifier = null,/// Distance in pixels or percentage adjustment from the top of the cell to the top of the overline./// Increase to move overline DOWN, decrease to move overline UP./// See the notes about adjustments in `adjust-cell-width`.@"adjust-overline-position": ?MetricModifier = null,/// Thickness in pixels or percentage adjustment of the overline./// See the notes about adjustments in `adjust-cell-width`.@"adjust-overline-thickness": ?MetricModifier = null,/// Thickness in pixels or percentage adjustment of the bar cursor and outlined rect cursor./// See the notes about adjustments in `adjust-cell-width`.@"adjust-cursor-thickness": ?MetricModifier = null,/// Height in pixels or percentage adjustment of the cursor. Currently applies to all cursor types:/// bar, rect, and outlined rect./// See the notes about adjustments in `adjust-cell-width`.@"adjust-cursor-height": ?MetricModifier = null,/// Thickness in pixels or percentage adjustment of box drawing characters./// See the notes about adjustments in `adjust-cell-width`.@"adjust-box-thickness": ?MetricModifier = null,/// Height in pixels or percentage adjustment of maximum height for nerd font icons.////// A positive (negative) value will increase (decrease) the maximum icon/// height. This may not affect all icons equally: the effect depends on whether/// the default size of the icon is height-constrained, which in turn depends on/// the aspect ratio of both the icon and your primary font.////// Certain icons designed for box drawing and terminal graphics, such as/// Powerline symbols, are not affected by this option.////// See the notes about adjustments in `adjust-cell-width`.////// Available in: 1.2.0@"adjust-icon-height": ?MetricModifier = null,
/// The method to use for calculating the cell width of a grapheme cluster./// The default value is `unicode` which uses the Unicode standard to determine/// grapheme width. This results in correct grapheme width but may result in/// cursor-desync issues with some programs (such as shells) that may use a/// legacy method such as `wcswidth`.////// Valid values are:////// * `legacy` - Use a legacy method to determine grapheme width, such as/// wcswidth This maximizes compatibility with legacy programs but may result/// in incorrect grapheme width for certain graphemes such as skin-tone/// emoji, non-English characters, etc.////// This is called "legacy" and not something more specific because the/// behavior is undefined and we want to retain the ability to modify it./// For example, we may or may not use libc `wcswidth` now or in the future.////// * `unicode` - Use the Unicode standard to determine grapheme width.////// If a running program explicitly enables terminal mode 2027, then `unicode`/// width will be forced regardless of this configuration. When mode 2027 is/// reset, this configuration will be used again.////// This configuration can be changed at runtime but will not affect existing/// terminals. Only new terminals will use the new configuration.@"grapheme-width-method": GraphemeWidthMethod = .unicode,
/// FreeType load flags to enable. The format of this is a list of flags to/// enable separated by commas. If you prefix a flag with `no-` then it is/// disabled. If you omit a flag, its default value is used, so you must/// explicitly disable flags you don't want. You can also use `true` or `false`/// to turn all flags on or off.////// This configuration only applies to Ghostty builds that use FreeType./// This is usually the case only for Linux builds. macOS uses CoreText/// and does not have an equivalent configuration.////// Available flags:////// * `hinting` - Enable or disable hinting. Enabled by default.////// * `force-autohint` - Always use the freetype auto-hinter instead of/// the font's native hinter. Disabled by default.////// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering./// This will disable anti-aliasing, and probably not look very good unless/// you're using a pixel font. Disabled by default.////// * `autohint` - Enable the freetype auto-hinter. Enabled by default.////// * `light` - Use a light hinting style, better preserving glyph shapes./// This is the most common setting in GTK apps and therefore also Ghostty's/// default. This has no effect if `monochrome` is enabled. Enabled by/// default.////// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint`@"freetype-load-flags": FreetypeLoadFlags = .{},
/// A theme to use. This can be a built-in theme name, a custom theme/// name, or an absolute path to a custom theme file. Ghostty also supports/// specifying a different theme to use for light and dark mode. Each/// option is documented below.////// If the theme is an absolute pathname, Ghostty will attempt to load that/// file as a theme. If that file does not exist or is inaccessible, an error/// will be logged and no other directories will be searched.////// If the theme is not an absolute pathname, two different directories will be/// searched for a file name that matches the theme. This is case sensitive on/// systems with case-sensitive filesystems. It is an error for a theme name to/// include path separators unless it is an absolute pathname.////// The first directory is the `themes` subdirectory of your Ghostty/// configuration directory. This is `$XDG_CONFIG_HOME/ghostty/themes` or/// `~/.config/ghostty/themes`.////// The second directory is the `themes` subdirectory of the Ghostty resources/// directory. Ghostty ships with a multitude of themes that will be installed/// into this directory. On macOS, this list is in the/// `Ghostty.app/Contents/Resources/ghostty/themes` directory. On Linux, this/// list is in the `share/ghostty/themes` directory (wherever you installed the/// Ghostty "share" directory.////// To see a list of available themes, run `ghostty +list-themes`.////// A theme file is simply another Ghostty configuration file. They share/// the same syntax and same configuration options. A theme can set any valid/// configuration option so please do not use a theme file from an untrusted/// source. The built-in themes are audited to only set safe configuration/// options.////// Some options cannot be set within theme files. The reason these are not/// supported should be self-evident. A theme file cannot set `theme` or/// `config-file`. At the time of writing this, Ghostty will not show any/// warnings or errors if you set these options in a theme file but they will/// be silently ignored.////// Any additional colors specified via background, foreground, palette, etc./// will override the colors specified in the theme.////// To specify a different theme for light and dark mode, use the following/// syntax: `light:theme-name,dark:theme-name`. For example:/// `light:Rose Pine Dawn,dark:Rose Pine`. Whitespace around all values are/// trimmed and order of light and dark does not matter. Both light and dark/// must be specified in this form. In this form, the theme used will be/// based on the current desktop environment theme.////// There are some known bugs with light/dark mode theming. These will/// be fixed in a future update:////// - macOS: titlebar tabs style is not updated when switching themes.theme: ?Theme = null,
/// Background color for the window./// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
/// Foreground color for the window./// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// Background image for the terminal.////// This should be a path to a PNG or JPEG file, other image formats are/// not yet supported.////// The background image is currently per-terminal, not per-window. If/// you are a heavy split user, the background image will be repeated across/// splits. A future improvement to Ghostty will address this.////// WARNING: Background images are currently duplicated in VRAM per-terminal./// For sufficiently large images, this could lead to a large increase in/// memory usage (specifically VRAM usage). A future Ghostty improvement/// will resolve this by sharing image textures across terminals.////// Available since: 1.2.0@"background-image": ?Path = null,
/// Background image opacity.////// This is relative to the value of `background-opacity`.////// A value of `1.0` (the default) will result in the background image being/// placed on top of the general background color, and then the combined result/// will be adjusted to the opacity specified by `background-opacity`.////// A value less than `1.0` will result in the background image being mixed/// with the general background color before the combined result is adjusted/// to the configured `background-opacity`.////// A value greater than `1.0` will result in the background image having a/// higher opacity than the general background color. For instance, if the/// configured `background-opacity` is `0.5` and `background-image-opacity`/// is set to `1.5`, then the final opacity of the background image will be/// `0.5 * 1.5 = 0.75`.////// Available since: 1.2.0@"background-image-opacity": f32 = 1.0,
/// Background image position.////// Valid values are:/// * `top-left`/// * `top-center`/// * `top-right`/// * `center-left`/// * `center`/// * `center-right`/// * `bottom-left`/// * `bottom-center`/// * `bottom-right`////// The default value is `center`.////// Available since: 1.2.0@"background-image-position": BackgroundImagePosition = .center,
/// Background image fit.////// Valid values are:////// * `contain`////// Preserving the aspect ratio, scale the background image to the largest/// size that can still be contained within the terminal, so that the whole/// image is visible.////// * `cover`////// Preserving the aspect ratio, scale the background image to the smallest/// size that can completely cover the terminal. This may result in one or/// more edges of the image being clipped by the edge of the terminal.////// * `stretch`////// Stretch the background image to the full size of the terminal, without/// preserving the aspect ratio.////// * `none`////// Don't scale the background image.////// The default value is `contain`.////// Available since: 1.2.0@"background-image-fit": BackgroundImageFit = .contain,
/// Whether to repeat the background image or not.////// If this is set to true, the background image will be repeated if there/// would otherwise be blank space around it because it doesn't completely/// fill the terminal area.////// The default value is `false`.////// Available since: 1.2.0@"background-image-repeat": bool = false,
/// The foreground and background color for selection. If this is not set, then/// the selection color is just the inverted window background and foreground/// (note: not to be confused with the cell bg/fg)./// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color./// Since version 1.2.0, this can also be set to `cell-foreground` to match/// the cell foreground color, or `cell-background` to match the cell/// background color.@"selection-foreground": ?TerminalColor = null,@"selection-background": ?TerminalColor = null,
/// Whether to clear selected text when typing. This defaults to `true`./// This is typical behavior for most terminal emulators as well as/// text input fields. If you set this to `false`, then the selected text/// will not be cleared when typing.////// "Typing" is specifically defined as any non-modifier (shift, control,/// alt, etc.) keypress that produces data to be sent to the application/// running within the terminal (e.g. the shell). Additionally, selection/// is cleared when any preedit or composition state is started (e.g./// when typing languages such as Japanese).////// If this is `false`, then the selection can still be manually/// cleared by clicking once or by pressing `escape`.////// Available since: 1.2.0@"selection-clear-on-typing": bool = true,
/// Whether to clear selected text after copying. This defaults to `false`.////// When set to `true`, the selection will be automatically cleared after/// any copy operation that invokes the `copy_to_clipboard` keyboard binding./// Importantly, this will not clear the selection if the copy operation/// was invoked via `copy-on-select`.////// When set to `false`, the selection remains visible after copying, allowing/// to see what was copied and potentially perform additional operations/// on the same selection.@"selection-clear-on-copy": bool = false,
/// Characters that mark word boundaries during text selection operations such/// as double-clicking. When selecting a word, the selection will stop at any/// of these characters.////// This is similar to the `WORDCHARS` environment variable in zsh, except this/// specifies the boundary characters rather than the word characters. The/// default includes common delimiters and punctuation that typically separate/// words in code and prose.////// Each character in this string becomes a word boundary. Multi-byte UTF-8/// characters are supported, but only single codepoints can be specified./// Multi-codepoint sequences (e.g. emoji) are not supported.////// The null character (U+0000) is always treated as a boundary and does not/// need to be included in this configuration.////// Default: ``\t '"│`|:;,()[]{}<>$``////// To add or remove specific characters, you can set this to a custom value./// For example, to treat semicolons as part of words:////// selection-word-chars = "\t '\"│`|:,()[]{}<>$"////// Available since: 1.3.0@"selection-word-chars": SelectionWordChars = .{},
/// The minimum contrast ratio between the foreground and background colors./// The contrast ratio is a value between 1 and 21. A value of 1 allows for no/// contrast (e.g. black on black). This value is the contrast ratio as defined/// by the [WCAG 2.0 specification](https://www.w3.org/TR/WCAG20/).////// If you want to avoid invisible text (same color as background), a value of/// 1.1 is a good value. If you want to avoid text that is difficult to read, a/// value of 3 or higher is a good value. The higher the value, the more likely/// that text will become black or white.////// This value does not apply to Emoji or images.@"minimum-contrast": f64 = 1,
/// Color palette for the 256 color form that many terminal applications use./// The syntax of this configuration is `N=COLOR` where `N` is 0 to 255 (for/// the 256 colors in the terminal color table) and `COLOR` is a typical RGB/// color code such as `#AABBCC` or `AABBCC`, or a named X11 color. For example,/// `palette = 5=#BB78D9` will set the 'purple' color.////// The palette index can be in decimal, binary, octal, or hexadecimal./// Decimal is assumed unless a prefix is used: `0b` for binary, `0o` for octal,/// and `0x` for hexadecimal.////// For definitions on the color indices and what they canonically map to,/// [see this cheat sheet](https://www.ditig.com/256-colors-cheat-sheet).////// For most themes, you only need to set the first 16 colors (0–15) since the/// rest of the palette (16–255) will be automatically generated by/// default (see `palette-generate` for more details).palette: Palette = .{},
/// Whether to automatically generate the extended 256 color palette/// (indices 16–255) from the base 16 ANSI colors.////// This lets theme authors specify only the base 16 colors and have the/// rest of the palette be automatically generated in a consistent and/// aesthetic way.////// When enabled, the 6×6×6 color cube and 24-step grayscale ramp are/// derived from interpolations of the base palette, giving a more cohesive/// look. Colors that have been explicitly set via `palette` are never/// overwritten.////// The default value is false (disabled), because many legacy programs/// using the 256-color palette hardcode assumptions about what these/// colors are (mostly assuming the xterm 256 color palette). However, this/// is still a very useful tool for theme authors and users who want/// to customize their palette without having to specify all 256 colors.////// For more information on how the generation works, see here:/// https://gist.github.com/jake-stewart/0a8ea46159a7da2c808e5be2177e1783////// Available since: 1.3.0@"palette-generate": bool = false,
/// Invert the palette colors generated when `palette-generate` is enabled,/// so that the colors go in reverse order. This allows palette-based/// applications to work well in both light and dark mode since the/// palettes are always relatively good colors.////// This defaults to off because some legacy terminal applications/// hardcode the assumption that palette indices 16–231 are ordered from/// darkest to lightest, so enabling this would make them unreadable./// This is not a generally good assumption and we encourage modern/// terminal applications to use the indices in a more semantic way.////// This has no effect if `palette-generate` is disabled.////// For more information see `palette-generate`.////// Available since: 1.3.0@"palette-harmonious": bool = false,
/// The color of the cursor. If this is not set, a default will be chosen.////// Direct colors can be specified as either hex (`#RRGGBB` or `RRGGBB`)/// or a named X11 color.////// Additionally, special values can be used to set the color to match/// other colors at runtime:////// * `cell-foreground` - Match the cell foreground color./// (Available since: 1.2.0)////// * `cell-background` - Match the cell background color./// (Available since: 1.2.0)@"cursor-color": ?TerminalColor = null,
/// The opacity level (opposite of transparency) of the cursor. A value of 1/// is fully opaque and a value of 0 is fully transparent. A value less than 0/// or greater than 1 will be clamped to the nearest valid value. Note that a/// sufficiently small value such as 0.3 may be effectively invisible and may/// make it difficult to find the cursor.@"cursor-opacity": f64 = 1.0,
/// The style of the cursor. This sets the default style. A running program can/// still request an explicit cursor style using escape sequences (such as `CSI/// q`). Shell configurations will often request specific cursor styles.////// Note that shell integration will automatically set the cursor to a bar at/// a prompt, regardless of this configuration. You can disable that behavior/// by specifying `shell-integration-features = no-cursor` or disabling shell/// integration entirely.////// Valid values are:////// * `block`/// * `bar`/// * `underline`/// * `block_hollow`@"cursor-style": terminal.CursorStyle = .block,
/// Sets the default blinking state of the cursor. This is just the default/// state; running programs may override the cursor style using `DECSCUSR` (`CSI/// q`).////// If this is not set, the cursor blinks by default. Note that this is not the/// same as a "true" value, as noted below.////// If this is not set at all (`null`), then Ghostty will respect DEC Mode 12/// (AT&T cursor blink) as an alternate approach to turning blinking on/off. If/// this is set to any value other than null, DEC mode 12 will be ignored but/// `DECSCUSR` will still be respected.////// Valid values are:////// * ` ` (blank)/// * `true`/// * `false`@"cursor-style-blink": ?bool = null,
/// The color of the text under the cursor. If this is not set, a default will/// be chosen./// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color./// Since version 1.2.0, this can also be set to `cell-foreground` to match/// the cell foreground color, or `cell-background` to match the cell/// background color.@"cursor-text": ?TerminalColor = null,
/// Enables the ability to move the cursor at prompts by clicking on a/// location in the prompt text.////// This feature requires shell integration, specifically prompt marking/// via `OSC 133`. Some shells like Fish (v4) and Nu (0.111+) natively/// support this while others may require additional configuration or/// Ghostty's shell integration features to be enabled.////// Depending on the shell, this works either by translating your click/// position into a series of synthetic arrow key movements or by sending/// a click event directly to the shell. In either case, some unexpected/// behavior around edge cases is possible.@"cursor-click-to-move": bool = true,
/// Hide the mouse immediately when typing. The mouse becomes visible again/// when the mouse is used (button, movement, etc.). Platform-specific behavior/// may dictate other scenarios where the mouse is shown. For example on macOS,/// the mouse is shown again when a new window, tab, or split is created.@"mouse-hide-while-typing": bool = false,
/// When to scroll the surface to the bottom. The format of this is a list of/// options to enable separated by commas. If you prefix an option with `no-`/// then it is disabled. If you omit an option, its default value is used.////// Available options:////// - `keystroke` If set, scroll the surface to the bottom when the user/// presses a key that results in data being sent to the PTY (basically/// anything but modifiers or keybinds that are processed by Ghostty).////// - `output` If set, scroll the surface to the bottom if there is new data/// to display (e.g., when new lines are printed to the terminal).////// The default is `keystroke, no-output`.@"scroll-to-bottom": ScrollToBottom = .default,
/// Determines whether running programs can detect the shift key pressed with a/// mouse click. Typically, the shift key is used to extend mouse selection.////// The default value of `false` means that the shift key is not sent with/// the mouse protocol and will extend the selection. This value can be/// conditionally overridden by the running program with the `XTSHIFTESCAPE`/// sequence.////// The value `true` means that the shift key is sent with the mouse protocol/// but the running program can override this behavior with `XTSHIFTESCAPE`.////// The value `never` is the same as `false` but the running program cannot/// override this behavior with `XTSHIFTESCAPE`. The value `always` is the/// same as `true` but the running program cannot override this behavior with/// `XTSHIFTESCAPE`.////// If you always want shift to extend mouse selection even if the program/// requests otherwise, set this to `never`.////// Valid values are:////// * `true`/// * `false`/// * `always`/// * `never`@"mouse-shift-capture": MouseShiftCapture = .false,
/// Enable or disable mouse reporting. When set to `false`, mouse events will/// not be reported to terminal applications even if they request it. This/// allows you to always use the mouse for selection and other terminal UI/// interactions without applications capturing mouse input.////// When set to `true` (the default), terminal applications can request mouse/// reporting and will receive mouse events according to their requested mode.////// This can be toggled at runtime using the `toggle_mouse_reporting` keybind/// action.@"mouse-reporting": bool = true,
/// Multiplier for scrolling distance with the mouse wheel.////// A prefix of `precision:` or `discrete:` can be used to set the multiplier/// only for scrolling with the specific type of devices. These can be/// comma-separated to set both types of multipliers at the same time, e.g./// `precision:0.1,discrete:3`. If no prefix is used, the multiplier applies/// to all scrolling devices. Specifying a prefix was introduced in Ghostty/// 1.2.1.////// The value will be clamped to [0.01, 10,000]. Both of these are extreme/// and you're likely to have a bad experience if you set either extreme.////// The default value is "3" for discrete devices and "1" for precision devices.@"mouse-scroll-multiplier": MouseScrollMultiplier = .default,
/// The opacity level (opposite of transparency) of the background. A value of/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0/// or greater than 1 will be clamped to the nearest valid value.////// On macOS, background opacity is disabled when the terminal enters native/// fullscreen. This is because the background becomes gray and it can cause/// widgets to show through which isn't generally desirable.////// On macOS, changing this configuration requires restarting Ghostty completely.@"background-opacity": f64 = 1.0,
/// Applies background opacity to cells with an explicit background color/// set.////// Normally, `background-opacity` is only applied to the window background./// If a cell has an explicit background color set, such as red, then that/// background color will be fully opaque. An effect of this is that some/// terminal applications that repaint the background color of the terminal/// such as a Neovim and Tmux may not respect the `background-opacity`/// (by design).////// Setting this to `true` will apply the `background-opacity` to all cells/// regardless of whether they have an explicit background color set or not.////// Available since: 1.2.0@"background-opacity-cells": bool = false,
/// Whether to blur the background when `background-opacity` is less than 1.////// Valid values are:////// * a nonnegative integer specifying the *blur intensity*/// * `false`, equivalent to a blur intensity of 0/// * `true`, equivalent to the default blur intensity of 20, which is/// reasonable for a good looking blur. Higher blur intensities may/// cause strange rendering and performance issues.////// On macOS 26.0 and later, there are additional special values that/// can be set to use the native macOS glass effects:////// * `macos-glass-regular` - Standard glass effect with some opacity/// * `macos-glass-clear` - Highly transparent glass effect////// If the macOS values are set, then this implies `background-blur = true`/// on non-macOS platforms.////// Supported on macOS and on some Linux desktop environments, including:////// * KDE Plasma (Wayland and X11)////// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting/// this setting to either `true` or any positive blur intensity value would/// achieve the same effect. The reason is that KWin, the window compositor/// powering Plasma, only has one global blur setting and does not allow/// applications to specify individual blur settings.////// To configure KWin's global blur setting, open System Settings and go to/// "Apps & Windows" > "Window Management" > "Desktop Effects" and select the/// "Blur" plugin. If disabled, enable it by ticking the checkbox to the left./// Then click on the "Configure" button and there will be two sliders that/// allow you to set background blur and noise intensities for all apps,/// including Ghostty.////// All other Linux desktop environments are as of now unsupported. Users may/// need to set environment-specific settings and/or install third-party plugins/// in order to support background blur, as there isn't a unified interface for/// doing so.@"background-blur": BackgroundBlur = .false,
/// The opacity level (opposite of transparency) of an unfocused split./// Unfocused splits by default are slightly faded out to make it easier to see/// which split is focused. To disable this feature, set this value to 1.////// A value of 1 is fully opaque and a value of 0 is fully transparent. Because/// "0" is not useful (it makes the window look very weird), the minimum value/// is 0.15. This value still looks weird but you can at least see what's going/// on. A value outside of the range 0.15 to 1 will be clamped to the nearest/// valid value.@"unfocused-split-opacity": f64 = 0.7,
/// The color to dim the unfocused split. Unfocused splits are dimmed by/// rendering a semi-transparent rectangle over the split. This sets the color of/// that rectangle and can be used to carefully control the dimming effect.////// This will default to the background color.////// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.@"unfocused-split-fill": ?Color = null,
/// The color of the split divider. If this is not set, a default will be chosen./// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.////// Available since: 1.1.0@"split-divider-color": ?Color = null,
/// Control when Ghostty preserves a zoomed split. Under normal circumstances,/// any operation that changes focus or layout of the split tree in a window/// will unzoom any zoomed split. This configuration allows you to control/// this behavior.////// This can be set to `navigation` to preserve the zoomed split state/// when navigating to another split (e.g. via `goto_split`). This will/// change the zoomed split to the newly focused split instead of unzooming.////// Any options can also be prefixed with `no-` to disable that option.////// Example: `split-preserve-zoom = navigation`////// Available since: 1.3.0@"split-preserve-zoom": SplitPreserveZoom = .{},
/// The foreground and background color for search matches. This only applies/// to non-focused search matches, also known as candidate matches.////// Valid values:////// - Hex (`#RRGGBB` or `RRGGBB`)/// - Named X11 color/// - "cell-foreground" to match the cell foreground color/// - "cell-background" to match the cell background color////// The default value is black text on a golden yellow background.@"search-foreground": TerminalColor = .{ .color = .{ .r = 0, .g = 0, .b = 0 } },@"search-background": TerminalColor = .{ .color = .{ .r = 0xFF, .g = 0xE0, .b = 0x82 } },
/// The foreground and background color for the currently selected search match./// This is the focused match that will be jumped to when using next/previous/// search navigation.////// Valid values:////// - Hex (`#RRGGBB` or `RRGGBB`)/// - Named X11 color/// - "cell-foreground" to match the cell foreground color/// - "cell-background" to match the cell background color////// The default value is black text on a soft peach background.@"search-selected-foreground": TerminalColor = .{ .color = .{ .r = 0, .g = 0, .b = 0 } },@"search-selected-background": TerminalColor = .{ .color = .{ .r = 0xF2, .g = 0xA5, .b = 0x7E } },
/// The command to run, usually a shell. If this is not an absolute path, it'll/// be looked up in the `PATH`. If this is not set, a default will be looked up/// from your system. The rules for the default lookup are:////// * `SHELL` environment variable////// * `passwd` entry (user information)////// This can contain additional arguments to run the command with. If additional/// arguments are provided, the command will be executed using `/bin/sh -c`/// to offload shell argument expansion.////// To avoid shell expansion altogether, prefix the command with `direct:`, e.g./// `direct:nvim foo`. This will avoid the roundtrip to `/bin/sh` but will also/// not support any shell parsing such as arguments with spaces, filepaths with/// `~`, globs, etc. (Available since: 1.2.0)////// You can also explicitly prefix the command with `shell:` to always wrap the/// command in a shell. This can be used to ensure our heuristics to choose the/// right mode are not used in case they are wrong. (Available since: 1.2.0)////// This command will be used for all new terminal surfaces, i.e. new windows,/// tabs, etc. If you want to run a command only for the first terminal surface/// created when Ghostty starts, use the `initial-command` configuration.////// Ghostty supports the common `-e` flag for executing a command with/// arguments. For example, `ghostty -e fish --with --custom --args`./// This flag sets the `initial-command` configuration, see that for more/// information.command: ?Command = null,
/// This is the same as "command", but only applies to the first terminal/// surface created when Ghostty starts. Subsequent terminal surfaces will use/// the `command` configuration.////// After the first terminal surface is created (or closed), there is no/// way to run this initial command again automatically. As such, setting/// this at runtime works but will only affect the next terminal surface/// if it is the first one ever created.////// If you're using the `ghostty` CLI there is also a shortcut to set this/// with arguments directly: you can use the `-e` flag. For example: `ghostty -e/// fish --with --custom --args`. The `-e` flag automatically forces some/// other behaviors as well:////// * Disables shell expansion since the input is expected to already/// be shell-expanded by the upstream (e.g. the shell used to type in/// the `ghostty -e` command).////// * `gtk-single-instance=false` - This ensures that a new instance is/// launched and the CLI args are respected.////// * `quit-after-last-window-closed=true` - This ensures that the Ghostty/// process will exit when the command exits. Additionally, the/// `quit-after-last-window-closed-delay` is unset.////// * `shell-integration=detect` (if not `none`) - This prevents forcibly/// injecting any configured shell integration into the command's/// environment. With `-e` its highly unlikely that you're executing a/// shell and forced shell integration is likely to cause problems/// (e.g. by wrapping your command in a shell, setting env vars, etc.)./// This is a safety measure to prevent unexpected behavior. If you want/// shell integration with a `-e`-executed command, you must either/// name your binary appropriately or source the shell integration script/// manually.@"initial-command": ?Command = null,
/// Controls when command finished notifications are sent. There are/// three options:////// * `never` - Never send notifications (the default)./// * `unfocused` - Only send notifications if the surface that the command is/// running in is not focused./// * `always` - Always send notifications.////// Command finished notifications requires that either shell integration is/// enabled, or that your shell sends OSC 133 escape sequences to mark the start/// and end of commands.////// On GTK, there is a context menu item that will enable command finished/// notifications for a single command, overriding the `never` and `unfocused`/// options.////// Available since 1.3.0.@"notify-on-command-finish": NotifyOnCommandFinish = .never,
/// If command finished notifications are enabled, this controls how the user is/// notified.////// Available options:////// * `bell` - enabled by default/// * `notify` - disabled by default////// Options can be combined by listing them as a comma separated list. Options/// can be negated by prefixing them with `no-`. For example `no-bell,notify`.////// Available since 1.3.0.@"notify-on-command-finish-action": NotifyOnCommandFinishAction = .{ .bell = true, .notify = false,},
/// If command finished notifications are enabled, this controls how long a/// command must have been running before a notification will be sent. The/// default is five seconds.////// The duration is specified as a series of numbers followed by time units./// Whitespace is allowed between numbers and units. Each number and unit will/// be added together to form the total duration.////// The allowed time units are as follows:////// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments/// are made for leap years or leap seconds./// * `d` - one SI day, or 86400 seconds./// * `h` - one hour, or 3600 seconds./// * `m` - one minute, or 60 seconds./// * `s` - one second./// * `ms` - one millisecond, or 0.001 second./// * `us` or `µs` - one microsecond, or 0.000001 second./// * `ns` - one nanosecond, or 0.000000001 second.////// Examples:/// * `1h30m`/// * `45s`////// Units can be repeated and will be added together. This means that/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided./// A future update may disallow this.////// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any/// value larger than this will be clamped to the maximum value.////// Available since 1.3.0@"notify-on-command-finish-after": Duration = .{ .duration = 5 * std.time.ns_per_s },
/// Extra environment variables to pass to commands launched in a terminal/// surface. The format is `env=KEY=VALUE`.////// `env = foo=bar`/// `env = bar=baz`////// Setting `env` to an empty string will reset the entire map to default/// (empty).////// `env =`////// Setting a key to an empty string will remove that particular key and/// corresponding value from the map.////// `env = foo=bar`/// `env = foo=`////// will result in `foo` not being passed to the launched commands.////// Setting a key multiple times will overwrite previous entries.////// `env = foo=bar`/// `env = foo=baz`////// will result in `foo=baz` being passed to the launched commands.////// These environment variables will override any existing environment/// variables set by Ghostty. For example, if you set `GHOSTTY_RESOURCES_DIR`/// then the value you set here will override the value Ghostty typically/// automatically injects.////// These environment variables _will not_ be passed to commands run by Ghostty/// for other purposes, like `open` or `xdg-open` used to open URLs in your/// browser.////// Available since: 1.2.0env: RepeatableStringMap = .{},
/// Data to send as input to the command on startup.////// The configured `command` will be launched using the typical rules,/// then the data specified as this input will be written to the pty/// before any other input can be provided.////// The bytes are sent as-is with no additional encoding. Therefore, be/// cautious about input that can contain control characters, because this/// can be used to execute programs in a shell.////// The format of this value is:////// * `raw:<string>` - Send raw text as-is. This uses Zig string literal/// syntax so you can specify control characters and other standard/// escapes.////// * `path:<path>` - Read a filepath and send the contents. The path/// must be to a file with finite length. e.g. don't use a device/// such as `/dev/stdin` or `/dev/urandom` as these will block/// terminal startup indefinitely. Files are limited to 10MB/// in size to prevent excessive memory usage. If you have files/// larger than this you should write a script to read the file/// and send it to the terminal.////// If no valid prefix is found, it is assumed to be a `raw:` input./// This is an ergonomic choice to allow you to simply write/// `input = "Hello, world!"` (a common case) without needing to prefix/// every value with `raw:`.////// This can be repeated multiple times to send more data. The data/// is concatenated directly with no separator characters in between/// (e.g. no newline).////// If any of the input sources do not exist, then none of the input/// will be sent. Input sources are not verified until the terminal/// is starting, so missing paths will not show up in config validation.////// Changing this configuration at runtime will only affect new/// terminals.////// Available since: 1.2.0input: RepeatableReadableIO = .{},
/// If true, keep the terminal open after the command exits. Normally, the/// terminal window closes when the running command (such as a shell) exits./// With this true, the terminal window will stay open until any keypress is/// received.////// This is primarily useful for scripts or debugging.@"wait-after-command": bool = false,
/// The number of milliseconds of runtime below which we consider a process exit/// to be abnormal. This is used to show an error message when the process exits/// too quickly.////// On Linux, this must be paired with a non-zero exit code. On macOS, we allow/// any exit code because of the way shell processes are launched via the login/// command.@"abnormal-command-exit-runtime": u32 = 250,
/// The size of the scrollback buffer in bytes. This also includes the active/// screen. No matter what this is set to, enough memory will always be/// allocated for the visible screen and anything leftover is the limit for/// the scrollback.////// When this limit is reached, the oldest lines are removed from the/// scrollback.////// Scrollback currently exists completely in memory. This means that the/// larger this value, the larger potential memory usage. Scrollback is/// allocated lazily up to this limit, so if you set this to a very large/// value, it will not immediately consume a lot of memory.////// This size is per terminal surface, not for the entire application.////// It is not currently possible to set an unlimited scrollback buffer./// This is a future planned feature.////// This can be changed at runtime but will only affect new terminal surfaces.@"scrollback-limit": usize = 10_000_000, // 10MB
/// Control when the scrollbar is shown to scroll the scrollback buffer.////// The default value is `system`.////// Valid values:////// * `system` - Respect the system settings for when to show scrollbars./// For example, on macOS, this will respect the "Scrollbar behavior"/// system setting which by default usually only shows scrollbars while/// actively scrolling or hovering the gutter.////// * `never` - Never show a scrollbar. You can still scroll using the mouse,/// keybind actions, etc. but you will not have a visual UI widget showing/// a scrollbar.scrollbar: Scrollbar = .system,
/// Match a regular expression against the terminal text and associate clicking/// it with an action. This can be used to match URLs, file paths, etc. Actions/// can be opening using the system opener (e.g. `open` or `xdg-open`) or/// executing any arbitrary binding action.////// Links that are configured earlier take precedence over links that are/// configured later.////// A default link that matches a URL and opens it in the system opener always/// exists. This can be disabled using `link-url`.////// TODO: This can't currently be set!link: RepeatableLink = .{},
/// Enable URL matching. URLs are matched on hover with control (Linux) or/// command (macOS) pressed and open using the default system application for/// the linked URL.////// The URL matcher is always lowest priority of any configured links (see/// `link`). If you want to customize URL matching, use `link` and disable this.@"link-url": bool = true,
/// Show link previews for a matched URL.////// When true, link previews are shown for all matched URLs. When false, link/// previews are never shown. When set to "osc8", link previews are only shown/// for hyperlinks created with the OSC 8 sequence (in this case, the link text/// can differ from the link destination).////// Available since: 1.2.0@"link-previews": LinkPreviews = .true,
/// Whether to start the window in a maximized state. This setting applies/// to new windows and does not apply to tabs, splits, etc. However, this setting/// will apply to all new windows, not just the first one.////// Available since: 1.1.0maximize: bool = false,
/// Start new windows in fullscreen. This setting applies to new windows and/// does not apply to tabs, splits, etc. However, this setting will apply to all/// new windows, not just the first one.////// Allowable values are:////// * `false` - Don't start in fullscreen (default)/// * `true` - Start in native fullscreen/// * `non-native` - (macOS only) Start in non-native fullscreen, hiding the/// menu bar. This is faster than native fullscreen since it doesn't use/// animations. On non-macOS platforms, this behaves the same as `true`./// * `non-native-visible-menu` - (macOS only) Start in non-native fullscreen,/// keeping the menu bar visible. On non-macOS platforms, behaves like `true`./// * `non-native-padded-notch` - (macOS only) Start in non-native fullscreen,/// hiding the menu bar but padding for the notch on applicable devices./// On non-macOS platforms, behaves like `true`.////// Important: tabs DO NOT WORK with non-native fullscreen modes. Non-native/// fullscreen removes the titlebar and macOS native tabs require the titlebar./// If you use tabs, use `true` (native) instead.////// On macOS, `true` (native fullscreen) does not work if `window-decoration`/// is set to `false`, because native fullscreen on macOS requires window/// decorations.fullscreen: Fullscreen = .false,
/// The title Ghostty will use for the window. This will force the title of the/// window to be this title at all times and Ghostty will ignore any set title/// escape sequences programs (such as Neovim) may send.////// If you want a blank title, set this to one or more spaces by quoting/// the value. For example, `title = " "`. This effectively hides the title./// This is necessary because setting a blank value resets the title to the/// default value of the running program.////// This configuration can be reloaded at runtime. If it is set, the title/// will update for all windows. If it is unset, the next title change escape/// sequence will be honored but previous changes will not retroactively/// be set. This latter case may require you to restart programs such as Neovim/// to get the new title.title: ?[:0]const u8 = null,
/// The setting that will change the application class value.////// This controls the class field of the `WM_CLASS` X11 property (when running/// under X11), the Wayland application ID (when running under Wayland), and the/// bus name that Ghostty uses to connect to DBus.////// Note that changing this value between invocations will create new, separate/// instances, of Ghostty when running with `gtk-single-instance=true`. See that/// option for more details.////// Changing this value may break launching Ghostty from `.desktop` files, via/// DBus activation, or systemd user services as the system is expecting Ghostty/// to connect to DBus using the default `class` when it is launched.////// The class name must follow the requirements defined [in the GTK/// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html).////// The default is `com.mitchellh.ghostty`.////// This only affects GTK builds.class: ?[:0]const u8 = null,
/// This controls the instance name field of the `WM_CLASS` X11 property when/// running under X11. It has no effect otherwise.////// The default is `ghostty`.////// This only affects GTK builds.@"x11-instance-name": ?[:0]const u8 = null,
/// The directory to change to after starting the command.////// This setting is secondary to the `window-inherit-working-directory`/// setting. If a previous Ghostty terminal exists in the same process,/// `window-inherit-working-directory` will take precedence. Otherwise, this/// setting will be used. Typically, this setting is used only for the first/// window.////// The default is `inherit` except in special scenarios listed next. On macOS,/// if Ghostty can detect it is launched from launchd (double-clicked) or/// `open`, then it defaults to `home`. On Linux with GTK, if Ghostty can detect/// it was launched from a desktop launcher, then it defaults to `home`.////// The value of this must be an absolute path, a path prefixed with `~/`/// (the tilde will be expanded to the user's home directory), or/// one of the special values below:////// * `home` - The home directory of the executing user.////// * `inherit` - The working directory of the launching process.@"working-directory": ?WorkingDirectory = null,
/// Key bindings. The format is `trigger=action`. Duplicate triggers will/// overwrite previously set values. The list of actions is available in/// the documentation or using the `ghostty +list-actions` command.////// Trigger: `+`-separated list of keys and modifiers. Example: `ctrl+a`,/// `ctrl+shift+b`, `up`.////// If the key is a single Unicode codepoint, the trigger will match/// any presses that produce that codepoint. These are impacted by/// keyboard layouts. For example, `a` will match the `a` key on a/// QWERTY keyboard, but will match the `q` key on a AZERTY keyboard/// (assuming US physical layout).////// For Unicode codepoints, matching is done by comparing the set of/// modifiers with the unmodified codepoint. The unmodified codepoint is/// sometimes called an "unshifted character" in other software, but all/// modifiers are considered, not only shift. For example, `ctrl+a` will match/// `a` but not `ctrl+shift+a` (which is `A` on a US keyboard).////// Further, codepoint matching is case-insensitive and the unmodified/// codepoint is always case folded for comparison. As a result,/// `ctrl+A` configured will match when `ctrl+a` is pressed. Note that/// this means some key combinations are impossible depending on keyboard/// layout. For example, `ctrl+_` is impossible on a US keyboard because/// `_` is `shift+-` and `ctrl+shift+-` is not equal to `ctrl+_` (because/// the modifiers don't match!). More details on impossible key combinations/// can be found at this excellent source written by Qt developers:/// https://doc.qt.io/qt-6/qkeysequence.html#keyboard-layout-issues////// Physical key codes can be specified by using any of the key codes/// as specified by the [W3C specification](https://www.w3.org/TR/uievents-code/)./// For example, `KeyA` will match the physical `a` key on a US standard/// keyboard regardless of the keyboard layout. These are case-sensitive.////// For aesthetic reasons, the w3c codes also support snake case. For/// example, `key_a` is equivalent to `KeyA`. The only exceptions are/// function keys, e.g. `F1` is `f1` (no underscore). This is a consequence/// of our internal code using snake case but is purposely supported/// and tested so it is safe to use. It allows an all-lowercase binding/// which I find more aesthetically pleasing.////// Function keys such as `insert`, `up`, `f5`, etc. are also specified/// using the keys as specified by the previously linked W3C specification.////// Physical keys always match with a higher priority than Unicode codepoints,/// so if you specify both `a` and `KeyA`, the physical key will always be used/// regardless of what order they are configured.////// The special key `catch_all` can be used to match any key that is not/// otherwise bound. This can be combined with modifiers, for example/// `ctrl+catch_all` will match any key pressed with `ctrl` that is not/// otherwise bound. When looking up a binding, Ghostty first tries to match/// `catch_all` with modifiers. If no match is found and the event has/// modifiers, it falls back to `catch_all` without modifiers.////// Valid modifiers are `shift`, `ctrl` (alias: `control`), `alt` (alias: `opt`,/// `option`), and `super` (alias: `cmd`, `command`). You may use the modifier/// or the alias. When debugging keybinds, the non-aliased modifier will always/// be used in output.////// Note: The fn or "globe" key on keyboards are not supported as a/// modifier. This is a limitation of the operating systems and GUI toolkits/// that Ghostty uses.////// Some additional notes for triggers:////// * modifiers cannot repeat, `ctrl+ctrl+a` is invalid.////// * modifiers and keys can be in any order, `shift+a+ctrl` is *weird*,/// but valid.////// * only a single key input is allowed, `ctrl+a+b` is invalid.////// You may also specify multiple triggers separated by `>` to require a/// sequence of triggers to activate the action. For example,/// `ctrl+a>n=new_window` will only trigger the `new_window` action if the/// user presses `ctrl+a` followed separately by `n`. In other software, this/// is sometimes called a leader key, a key chord, a key table, etc. There/// is no hardcoded limit on the number of parts in a sequence.////// Warning: If you define a sequence as a CLI argument to `ghostty`,/// you probably have to quote the keybind since `>` is a special character/// in most shells. Example: ghostty --keybind='ctrl+a>n=new_window'////// A trigger sequence has some special handling:////// * Ghostty will wait an indefinite amount of time for the next key in/// the sequence. There is no way to specify a timeout. The only way to/// force the output of a prefix key is to assign another keybind to/// specifically output that key (e.g. `ctrl+a>ctrl+a=text:foo`) or/// press an unbound key which will send both keys to the program.////// * If an unbound key is pressed during a sequence and a `catch_all`/// binding exists that would `ignore` the input, the entire sequence/// is dropped and nothing happens. Otherwise, the entire sequence is/// encoded and sent to the running program as if no keybind existed.////// * If a prefix in a sequence is previously bound, the sequence will/// override the previous binding. For example, if `ctrl+a` is bound to/// `new_window` and `ctrl+a>n` is bound to `new_tab`, pressing `ctrl+a`/// will do nothing.////// * Adding to the above, if a previously bound sequence prefix is/// used in a new, non-sequence binding, the entire previously bound/// sequence will be unbound. For example, if you bind `ctrl+a>n` and/// `ctrl+a>t`, and then bind `ctrl+a` directly, both `ctrl+a>n` and/// `ctrl+a>t` will become unbound.////// * Trigger sequences are not allowed for `global:` or `all:`-prefixed/// triggers. This is a limitation we could remove in the future.////// Action is the action to take when the trigger is satisfied. It takes the/// format `action` or `action:param`. The latter form is only valid if the/// action requires a parameter.////// * `ignore` - Do nothing, ignore the key input. This can be used to/// black hole certain inputs to have no effect.////// * `unbind` - Remove the binding. This makes it so the previous action/// is removed, and the key will be sent through to the child command/// if it is printable. Unbind will remove any matching trigger,/// including `physical:`-prefixed triggers without specifying the/// prefix.////// * `csi:text` - Send a CSI sequence. e.g. `csi:A` sends "cursor up".////// * `esc:text` - Send an escape sequence. e.g. `esc:d` deletes to the/// end of the word to the right.////// * `text:text` - Send a string. Uses Zig string literal syntax./// e.g. `text:\x15` sends Ctrl-U.////// * All other actions can be found in the documentation or by using the/// `ghostty +list-actions` command.////// Some notes for the action:////// * The parameter is taken as-is after the `:`. Double quotes or/// other mechanisms are included and NOT parsed. If you want to/// send a string value that includes spaces, wrap the entire/// trigger/action in double quotes. Example: `--keybind="up=csi:A B"`////// There are some additional special values that can be specified for/// keybind:////// * `keybind=clear` will clear all set keybindings. Warning: this/// removes ALL keybindings up to this point, including the default/// keybindings.////// The keybind trigger can be prefixed with some special values to change/// the behavior of the keybind. These are:////// * `all:`////// Make the keybind apply to all terminal surfaces. By default,/// keybinds only apply to the focused terminal surface. If this is true,/// then the keybind will be sent to all terminal surfaces. This only/// applies to actions that are surface-specific. For actions that/// are already global (e.g. `quit`), this prefix has no effect.////// Available since: 1.0.0////// * `global:`////// Make the keybind global. By default, keybinds only work within Ghostty/// and under the right conditions (application focused, sometimes terminal/// focused, etc.). If you want a keybind to work globally across your system/// (e.g. even when Ghostty is not focused), specify this prefix./// This prefix implies `all:`.////// Note: this does not work in all environments; see the additional notes/// below for more information.////// Available since: 1.0.0 on macOS, 1.2.0 on GTK////// * `unconsumed:`////// Do not consume the input. By default, a keybind will consume the input,/// meaning that the associated encoding (if any) will not be sent to the/// running program in the terminal. If you wish to send the encoded value/// to the program, specify the `unconsumed:` prefix before the entire/// keybind. For example: `unconsumed:ctrl+a=reload_config`. `global:` and/// `all:`-prefixed keybinds will always consume the input regardless of/// this setting. Since they are not associated with a specific terminal/// surface, they're never encoded.////// Available since: 1.0.0////// * `performable:`////// Only consume the input if the action is able to be performed./// For example, the `copy_to_clipboard` action will only consume the input/// if there is a selection to copy. If there is no selection, Ghostty/// behaves as if the keybind was not set. This has no effect with `global:`/// or `all:`-prefixed keybinds. For key sequences, this will reset the/// sequence if the action is not performable (acting identically to not/// having a keybind set at all).////// Performable keybinds will not appear as menu shortcuts in the/// application menu. This is because the menu shortcuts force the/// action to be performed regardless of the state of the terminal./// Performable keybinds will still work, they just won't appear as/// a shortcut label in the menu.////// Available since: 1.1.0////// Keybind triggers are not unique per prefix combination. For example,/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind/// set later will overwrite the keybind set earlier. In this case, the/// `global:` keybind will be used.////// Multiple prefixes can be specified. For example,/// `global:unconsumed:ctrl+a=reload_config` will make the keybind global/// and not consume the input to reload the config.////// Note: `global:` is only supported on macOS and certain Linux platforms.////// On macOS, this feature requires accessibility permissions to be granted/// to Ghostty. When a `global:` keybind is specified and Ghostty is launched/// or reloaded, Ghostty will attempt to request these permissions./// If the permissions are not granted, the keybind will not work. On macOS,/// you can find these permissions in System Preferences -> Privacy & Security/// -> Accessibility.////// On Linux, you need a desktop environment that implements the/// [Global Shortcuts](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html)/// protocol as a part of its XDG desktop protocol implementation./// Desktop environments that are known to support (or not support)/// global shortcuts include:////// - Users using KDE Plasma (since [5.27](https://kde.org/announcements/plasma/5/5.27.0/#wayland))/// and GNOME (since [48](https://release.gnome.org/48/#and-thats-not-all)) should be able/// to use global shortcuts with little to no configuration.////// - Some manual configuration is required on Hyprland. Consult the steps/// outlined on the [Hyprland Wiki](https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts)/// to set up global shortcuts correctly./// (Important: [`xdg-desktop-portal-hyprland`](https://wiki.hyprland.org/Hypr-Ecosystem/xdg-desktop-portal-hyprland/)/// must also be installed!)////// - Notably, global shortcuts have not been implemented on wlroots-based/// compositors like Sway (see [upstream issue](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)).////// ## Chained Actions////// A keybind can have multiple actions by using the `chain` keyword for/// subsequent actions. When a keybind is activated, all chained actions are/// executed in order. The syntax is:////// ```ini/// keybind = ctrl+a=new_window/// keybind = chain=goto_split:left/// ```////// This binds `ctrl+a` to first open a new window, then move focus to the/// left split. Each `chain` entry appends an action to the most recently/// defined keybind. You can chain as many actions as you want:////// ```ini/// keybind = ctrl+a=new_window/// keybind = chain=goto_split:left/// keybind = chain=toggle_fullscreen/// ```////// Chained actions cannot have prefixes like `global:` or `unconsumed:`./// The flags from the original keybind apply to the entire chain.////// Chained actions work with key sequences as well. For example:////// ```ini/// keybind = ctrl+a>n=new_window/// keybind = chain=goto_split:left/// ````////// Chains with key sequences apply to the most recent binding in the/// sequence.////// Chained keybinds are available since Ghostty 1.3.0.////// ## Key Tables////// You may also create a named set of keybindings known as a "key table."/// A key table must be explicitly activated for the bindings to become/// available. This can be used to implement features such as a/// "copy mode", "vim mode", etc. Generically, this can implement modal/// keyboard input.////// Key tables are defined using the syntax `<table>/<binding>`. The/// `<binding>` value is everything documented above for keybinds. The/// `<table>` value is the name of the key table. Table names can contain/// anything except `/`, `=`, `+`, and `>`. The characters `+` and `>` are/// reserved for keybind syntax (modifier combinations and key sequences)./// For example `foo/ctrl+a=new_window` defines a binding within a table/// named `foo`.////// Tables are activated and deactivated using the binding actions/// `activate_key_table:<name>` and `deactivate_key_table`. Other table/// related binding actions also exist; see the documentation for a full list./// These are the primary way to interact with key tables.////// Binding lookup proceeds from the innermost table outward, so keybinds in/// the default table remain available unless explicitly unbound in an inner/// table.////// A key table has some special syntax and handling:////// * `<name>/` (with no binding) defines and clears a table, resetting all/// of its keybinds and settings.////// * You cannot activate a table that is already the innermost table; such/// attempts are ignored. However, the same table can appear multiple times/// in the stack as long as it is not innermost (e.g., `A -> B -> A -> B`/// is valid, but `A -> B -> B` is not).////// * A table can be activated in one-shot mode using/// `activate_key_table_once:<name>`. A one-shot table is automatically/// deactivated when any non-catch-all binding is invoked.////// * Key sequences work within tables: `foo/ctrl+a>ctrl+b=new_window`./// If an invalid key is pressed, the sequence ends but the table remains/// active.////// * Chain actions work within tables, the `chain` keyword applies to/// the most recently defined binding in the table. e.g. if you set/// `table/ctrl+a=new_window` you can chain by using `chain=text:hello`./// Important: chain itself doesn't get prefixed with the table name,/// since it applies to the most recent binding in any table.////// * Prefixes like `global:` work within tables:/// `foo/global:ctrl+a=new_window`.////// Key tables are available since Ghostty 1.3.0.keybind: Keybinds = .{},
/// Remap modifier keys within Ghostty. This allows you to swap or reassign/// modifier keys at the application level without affecting system-wide/// settings.////// The format is `from=to` where both `from` and `to` are modifier key names./// You can use generic names like `ctrl`, `alt`, `shift`, `super` (macOS:/// `cmd`/`command`) or sided names like `left_ctrl`, `right_alt`, etc.////// This will NOT change keyboard layout or key encodings outside of Ghostty./// For example, on macOS, `option+a` may still produce `å` even if `option` is/// remapped to `ctrl`. Desktop environments usually handle key layout long/// before Ghostty receives the key events.////// Example:////// key-remap = ctrl=super/// key-remap = left_control=right_alt////// Important notes:////// * This is a one-way remap. If you remap `ctrl=super`, then the physical/// Ctrl key acts as Super, but the Super key remains Super.////// * Remaps are not transitive. If you remap `ctrl=super` and `alt=ctrl`,/// pressing Alt will produce Ctrl, NOT Super.////// * This affects both keybind matching and terminal input encoding./// This does NOT impact keyboard layout or how keys are interpreted/// prior to Ghostty receiving them. For example, `option+a` on macOS/// may still produce `å` even if `option` is remapped to `ctrl`.////// * Generic modifiers (e.g. `ctrl`) match both left and right physical keys./// Use sided names (e.g. `left_ctrl`) to remap only one side.////// There are other edge case scenarios that may not behave as expected/// but are working as intended the way this feature is designed:////// * On macOS, bindings in the main menu will trigger before any remapping/// is done. This is because macOS itself handles menu activation and/// this happens before Ghostty receives the key event. To workaround/// this, you should unbind the menu items and rebind them using your/// desired modifier.////// This configuration can be repeated to specify multiple remaps.@"key-remap": KeyRemapSet = .empty,
/// Horizontal window padding. This applies padding between the terminal cells/// and the left and right window borders. The value is in points, meaning that/// it will be scaled appropriately for screen DPI.////// If this value is set too large, the screen will render nothing, because the/// grid will be completely squished by the padding. It is up to you as the user/// to pick a reasonable value. If you pick an unreasonable value, a warning/// will appear in the logs.////// Changing this configuration at runtime will only affect new terminals, i.e./// new windows, tabs, etc.////// To set a different left and right padding, specify two numerical values/// separated by a comma. For example, `window-padding-x = 2,4` will set the/// left padding to 2 and the right padding to 4. If you want to set both/// paddings to the same value, you can use a single value. For example,/// `window-padding-x = 2` will set both paddings to 2.@"window-padding-x": WindowPadding = .{ .top_left = 2, .bottom_right = 2 },
/// Vertical window padding. This applies padding between the terminal cells and/// the top and bottom window borders. The value is in points, meaning that it/// will be scaled appropriately for screen DPI.////// If this value is set too large, the screen will render nothing, because the/// grid will be completely squished by the padding. It is up to you as the user/// to pick a reasonable value. If you pick an unreasonable value, a warning/// will appear in the logs.////// Changing this configuration at runtime will only affect new terminals,/// i.e. new windows, tabs, etc.////// To set a different top and bottom padding, specify two numerical values/// separated by a comma. For example, `window-padding-y = 2,4` will set the/// top padding to 2 and the bottom padding to 4. If you want to set both/// paddings to the same value, you can use a single value. For example,/// `window-padding-y = 2` will set both paddings to 2.@"window-padding-y": WindowPadding = .{ .top_left = 2, .bottom_right = 2 },
/// The viewport dimensions are usually not perfectly divisible by the cell/// size. In this case, some extra padding on the end of a column and the bottom/// of the final row may exist. If this is `true`, then this extra padding/// is automatically balanced between all four edges to minimize imbalance on/// one side. If this is `false`, the top left grid cell will always hug the/// edge with zero padding other than what may be specified with the other/// `window-padding` options.////// If other `window-padding` fields are set and this is `true`, this will still/// apply. The other padding is applied first and may affect how many grid cells/// actually exist, and this is applied last in order to balance the padding/// given a certain viewport size and grid cell size.////// Valid values are:////// * `false` - No balancing is applied./// * `true` - Balance the padding, but cap the top padding to avoid/// excessive space above the first row. Any excess is shifted to the/// bottom./// * `equal` - Balance the padding equally on all sides without any/// top-padding cap. (Available since: 1.4.0)@"window-padding-balance": WindowPaddingBalance = .false,
/// The color of the padding area of the window. Valid values are:////// * `background` - The background color specified in `background`./// * `extend` - Extend the background color of the nearest grid cell./// * `extend-always` - Same as "extend" but always extends without applying/// any of the heuristics that disable extending noted below.////// The "extend" value will be disabled in certain scenarios. On primary/// screen applications (e.g. not something like Neovim), the color will not/// be extended vertically if any of the following are true:////// * The nearest row has any cells that have the default background color./// The thinking is that in this case, the default background color looks/// fine as a padding color./// * The nearest row is a prompt row (requires shell integration). The/// thinking here is that prompts often contain powerline glyphs that/// do not look good extended./// * The nearest row contains a perfect fit powerline character. These/// don't look good extended.@"window-padding-color": WindowPaddingColor = .background,
/// Synchronize rendering with the screen refresh rate. If true, this will/// minimize tearing and align redraws with the screen but may cause input/// latency. If false, this will maximize redraw frequency but may cause tearing,/// and under heavy load may use more CPU and power.////// This defaults to true because out-of-sync rendering on macOS can/// cause kernel panics (macOS 14.4+) and performance issues for external/// displays over some hardware such as DisplayLink. If you want to minimize/// input latency, set this to false with the known aforementioned risks.////// Changing this value at runtime will only affect new terminals.////// This setting is only supported currently on macOS.@"window-vsync": bool = true,
/// If true, new windows will inherit the working directory of the/// previously focused window. If no window was previously focused, the default/// working directory will be used (the `working-directory` option).@"window-inherit-working-directory": bool = true,
/// If true, new tabs will inherit the working directory of the/// previously focused tab. If no tab was previously focused, the default/// working directory will be used (the `working-directory` option).@"tab-inherit-working-directory": bool = true,
/// If true, new split panes will inherit the working directory of the/// previously focused split. If no split was previously focused, the default/// working directory will be used (the `working-directory` option).@"split-inherit-working-directory": bool = true,
/// If true, new windows and tabs will inherit the font size of the previously/// focused window. If no window was previously focused, the default font size/// will be used. If this is false, the default font size specified in the/// configuration `font-size` will be used.@"window-inherit-font-size": bool = true,
/// Configure a preference for window decorations. This setting specifies/// a _preference_; the actual OS, desktop environment, window manager, etc./// may override this preference. Ghostty will do its best to respect this/// preference but it may not always be possible.////// Valid values:////// * `none`////// All window decorations will be disabled. Titlebar, borders, etc. will/// not be shown. On macOS, this will also disable tabs (enforced by the/// system).////// * `auto`////// Automatically decide to use either client-side or server-side/// decorations based on the detected preferences of the current OS and/// desktop environment. This option usually makes Ghostty look the most/// "native" for your desktop.////// * `client`////// Prefer client-side decorations.////// Available since: 1.1.0////// * `server`////// Prefer server-side decorations. This is only relevant on Linux with GTK,/// either on X11, or Wayland on a compositor that supports the/// `org_kde_kwin_server_decoration` protocol (e.g. KDE Plasma, but almost/// any non-GNOME desktop supports this protocol).////// If `server` is set but the environment doesn't support server-side/// decorations, client-side decorations will be used instead.////// Available since: 1.1.0////// The default value is `auto`.////// For the sake of backwards compatibility and convenience, this setting also/// accepts boolean true and false values. If set to `true`, this is equivalent/// to `auto`. If set to `false`, this is equivalent to `none`./// This is convenient for users who live primarily on systems that don't/// differentiate between client and server-side decorations (e.g. macOS and/// Windows).////// The "toggle_window_decorations" keybind action can be used to create/// a keybinding to toggle this setting at runtime.////// macOS: To hide the titlebar without removing the native window borders/// or rounded corners, use `macos-titlebar-style = hidden` instead.@"window-decoration": WindowDecoration = .auto,
/// The font that will be used for the application's window and tab titles.////// If this setting is left unset, the system default font will be used.////// Note: any font available on the system may be used, this font is not/// required to be a fixed-width font.////// Available since: 1.0.0 on macOS, 1.1.0 on GTK@"window-title-font-family": ?[:0]const u8 = null,
/// The text that will be displayed in the subtitle of the window. Valid values:////// * `false` - Disable the subtitle./// * `working-directory` - Set the subtitle to the working directory of the/// surface.////// This feature is only supported on GTK.////// Available since: 1.1.0@"window-subtitle": WindowSubtitle = .false,
/// The theme to use for the windows. Valid values:////// * `auto` - Determine the theme based on the configured terminal/// background color. This has no effect if the "theme" configuration/// has separate light and dark themes. In that case, the behavior/// of "auto" is equivalent to "system"./// * `system` - Use the system theme./// * `light` - Use the light theme regardless of system theme./// * `dark` - Use the dark theme regardless of system theme./// * `ghostty` - Use the background and foreground colors specified in the/// Ghostty configuration. This is only supported on Linux builds.////// On macOS, if `macos-titlebar-style` is `tabs` or `transparent`, the window theme will be/// automatically set based on the luminosity of the terminal background color./// This only applies to terminal windows. This setting will still apply to/// non-terminal windows within Ghostty.////// This is currently only supported on macOS and Linux.@"window-theme": WindowTheme = .auto,
/// The color space to use when interpreting terminal colors. "Terminal colors"/// refers to colors specified in your configuration and colors produced by/// direct-color SGR sequences.////// Valid values:////// * `srgb` - Interpret colors in the sRGB color space. This is the default./// * `display-p3` - Interpret colors in the Display P3 color space.////// This setting is currently only supported on macOS.@"window-colorspace": WindowColorspace = .srgb,
/// The initial window size. This size is in terminal grid cells by default./// Both values must be set to take effect. If only one value is set, it is/// ignored.////// We don't currently support specifying a size in pixels but a future change/// can enable that. If this isn't specified, the app runtime will determine/// some default size.////// Note that the window manager may put limits on the size or override the/// size. For example, a tiling window manager may force the window to be a/// certain size to fit within the grid. There is nothing Ghostty will do about/// this, but it will make an effort.////// Sizes larger than the screen size will be clamped to the screen size./// This can be used to create a maximized-by-default window size.////// This will not affect new tabs, splits, or other nested terminal elements./// This only affects the initial window size of any new window. Changing this/// value will not affect the size of the window after it has been created. This/// is only used for the initial size.////// BUG: On Linux with GTK, the calculated window size will not properly take/// into account window decorations. As a result, the grid dimensions will not/// exactly match this configuration. If window decorations are disabled (see/// `window-decoration`), then this will work as expected.////// Windows smaller than 10 wide by 4 high are not allowed.@"window-height": u32 = 0,@"window-width": u32 = 0,
/// The starting window position. This position is in pixels and is relative/// to the top-left corner of the primary monitor. Both values must be set to take/// effect. If only one value is set, it is ignored.////// Note that the window manager may put limits on the position or override/// the position. For example, a tiling window manager may force the window/// to be a certain position to fit within the grid. There is nothing Ghostty/// will do about this, but it will make an effort.////// Also note that negative values are also up to the operating system and/// window manager. Some window managers may not allow windows to be placed/// off-screen.////// Invalid positions are runtime-specific, but generally the positions are/// clamped to the nearest valid position.////// On macOS, the window position is relative to the top-left corner of/// the visible screen area. This means that if the menu bar is visible, the/// window will be placed below the menu bar.////// Note: this is only supported on macOS. The GTK runtime does not support/// setting the window position, as windows are only allowed position/// themselves in X11 and not Wayland.@"window-position-x": ?i16 = null,@"window-position-y": ?i16 = null,
/// Whether to enable saving and restoring window state. Window state includes/// their position, size, tabs, splits, etc. Some window state requires shell/// integration, such as preserving working directories. See `shell-integration`/// for more information.////// There are three valid values for this configuration:////// * `default` will use the default system behavior. On macOS, this/// will only save state if the application is forcibly terminated/// or if it is configured systemwide via Settings.app.////// * `never` will never save window state.////// * `always` will always save window state whenever Ghostty is exited.////// If you change this value to `never` while Ghostty is not running, the next/// Ghostty launch will NOT restore the window state.////// If you change this value to `default` while Ghostty is not running and the/// previous exit saved state, the next Ghostty launch will still restore the/// window state. This is because Ghostty cannot know if the previous exit was/// due to a forced save or not (macOS doesn't provide this information).////// If you change this value so that window state is saved while Ghostty is not/// running, the previous window state will not be restored because Ghostty only/// saves state on exit if this is enabled.////// The default value is `default`.////// This is currently only supported on macOS. This has no effect on Linux.@"window-save-state": WindowSaveState = .default,
/// Resize the window in discrete increments of the focused surface's cell size./// If this is disabled, surfaces are resized in pixel increments. Currently/// only supported on macOS.@"window-step-resize": bool = false,
/// The position where new tabs are created. Valid values:////// * `current` - Insert the new tab after the currently focused tab,/// or at the end if there are no focused tabs.////// * `end` - Insert the new tab at the end of the tab list.@"window-new-tab-position": WindowNewTabPosition = .current,
/// Whether to show the tab bar.////// Valid values:////// - `always`////// Always display the tab bar, even when there's only one tab.////// Available since: 1.2.0////// - `auto` *(default)*////// Automatically show and hide the tab bar. The tab bar is only/// shown when there are two or more tabs present.////// - `never`////// Never show the tab bar. Tabs are only accessible via the tab/// overview or by keybind actions.////// Currently only supported on Linux (GTK).@"window-show-tab-bar": WindowShowTabBar = .auto,
/// Background color for the window titlebar. This only takes effect if/// window-theme is set to ghostty. Currently only supported in the GTK app/// runtime.////// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.@"window-titlebar-background": ?Color = null,
/// Foreground color for the window titlebar. This only takes effect if/// window-theme is set to ghostty. Currently only supported in the GTK app/// runtime.////// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.@"window-titlebar-foreground": ?Color = null,
/// This controls when resize overlays are shown. Resize overlays are a/// transient popup that shows the size of the terminal while the surfaces are/// being resized. The possible options are:////// * `always` - Always show resize overlays./// * `never` - Never show resize overlays./// * `after-first` - The resize overlay will not appear when the surface/// is first created, but will show up if the surface is/// subsequently resized.////// The default is `after-first`.@"resize-overlay": ResizeOverlay = .@"after-first",
/// If resize overlays are enabled, this controls the position of the overlay./// The possible options are:////// * `center`/// * `top-left`/// * `top-center`/// * `top-right`/// * `bottom-left`/// * `bottom-center`/// * `bottom-right`////// The default is `center`.@"resize-overlay-position": ResizeOverlayPosition = .center,
/// If resize overlays are enabled, this controls how long the overlay is/// visible on the screen before it is hidden. The default is ¾ of a second or/// 750 ms.////// The duration is specified as a series of numbers followed by time units./// Whitespace is allowed between numbers and units. Each number and unit will/// be added together to form the total duration.////// The allowed time units are as follows:////// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments/// are made for leap years or leap seconds./// * `d` - one SI day, or 86400 seconds./// * `h` - one hour, or 3600 seconds./// * `m` - one minute, or 60 seconds./// * `s` - one second./// * `ms` - one millisecond, or 0.001 second./// * `us` or `µs` - one microsecond, or 0.000001 second./// * `ns` - one nanosecond, or 0.000000001 second.////// Examples:/// * `1h30m`/// * `45s`////// Units can be repeated and will be added together. This means that/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided./// A future update may disallow this.////// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any/// value larger than this will be clamped to the maximum value.////// Available since 1.0.0@"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms },
/// If true, when there are multiple split panes, the mouse selects the pane/// that is focused. This only applies to the currently focused window; e.g./// mousing over a split in an unfocused window will not focus that split/// and bring the window to front.////// Default is false.@"focus-follows-mouse": bool = false,
/// Whether to allow programs running in the terminal to read/write to the/// system clipboard (OSC 52, for googling). The default is to allow clipboard/// reading after prompting the user and allow writing unconditionally.////// Valid values are:////// * `ask`/// * `allow`/// * `deny`///@"clipboard-read": ClipboardAccess = .ask,@"clipboard-write": ClipboardAccess = .allow,
/// Trims trailing whitespace on data that is copied to the clipboard. This does/// not affect data sent to the clipboard via `clipboard-write`. This only/// applies to trailing whitespace on lines that have other characters./// Completely blank lines always have their whitespace trimmed.@"clipboard-trim-trailing-spaces": bool = true,
/// Require confirmation before pasting text that appears unsafe. This helps/// prevent a "copy/paste attack" where a user may accidentally execute unsafe/// commands by pasting text with newlines.@"clipboard-paste-protection": bool = true,
/// If true, bracketed pastes will be considered safe. By default, bracketed/// pastes are considered safe. "Bracketed" pastes are pastes while the running/// program has bracketed paste mode enabled (a setting set by the running/// program, not the terminal emulator).@"clipboard-paste-bracketed-safe": bool = true,
/// Enables or disabled title reporting (CSI 21 t). This escape sequence/// allows the running program to query the terminal title. This is a common/// security issue and is disabled by default.////// Warning: This can expose sensitive information at best and enable/// arbitrary code execution at worst (with a maliciously crafted title/// and a minor amount of user interaction).////// Available since: 1.0.1@"title-report": bool = false,
/// The total amount of bytes that can be used for image data (e.g. the Kitty/// image protocol) per terminal screen. The maximum value is 4,294,967,295/// (4GiB). The default is 320MB. If this is set to zero, then all image/// protocols will be disabled.////// This value is separate for primary and alternate screens so the effective/// limit per surface is double.@"image-storage-limit": u32 = 320 * 1000 * 1000,
/// Whether to automatically copy selected text to the clipboard. `true`/// will prefer to copy to the selection clipboard, otherwise it will copy to/// the system clipboard.////// The value `clipboard` will always copy text to the selection clipboard/// as well as the system clipboard.////// Middle-click primary paste (see `middle-click-action`) is enabled by/// default even if this is `false`. The clipboard it pastes from follows/// this setting: with `true` (or `false`) it reads from the selection/// clipboard (falling back to the system clipboard on platforms without a/// selection clipboard); with `clipboard` it reads from the system/// clipboard.////// The default value is true on Linux and macOS.@"copy-on-select": CopyOnSelect = switch (builtin.os.tag) { .linux => .true, .macos => .true, else => .false,},
/// The action to take when the user right-clicks on the terminal surface.////// Valid values:/// * `context-menu` - Show the context menu./// * `paste` - Paste the contents of the clipboard./// * `copy` - Copy the selected text to the clipboard./// * `copy-or-paste` - If there is a selection, copy the selected text to/// the clipboard; otherwise, paste the contents of the clipboard./// * `ignore` - Do nothing, ignore the right-click.////// The default value is `context-menu`.@"right-click-action": RightClickAction = .@"context-menu",
/// The action to take when the user middle-clicks on the terminal surface.////// Valid values:/// * `primary-paste` - Paste from the selection (or system) clipboard per/// `copy-on-select`./// * `ignore` - Do nothing, ignore the middle click.////// The default value is `primary-paste`.@"middle-click-action": MiddleClickAction = .@"primary-paste",
/// The time in milliseconds between clicks to consider a click a repeat/// (double, triple, etc.) or an entirely new single click. A value of zero will/// use a platform-specific default. The default on macOS is determined by the/// OS settings. On every other platform it is 500ms.@"click-repeat-interval": u32 = 0,
/// Additional configuration files to read. This configuration can be repeated/// to read multiple configuration files. Configuration files themselves can/// load more configuration files. Paths are relative to the file containing the/// `config-file` directive. For command-line arguments, paths are relative to/// the current working directory.////// Prepend a ? character to the file path to suppress errors if the file does/// not exist. If you want to include a file that begins with a literal ?/// character, surround the file path in double quotes (").////// Cycles are not allowed. If a cycle is detected, an error will be logged and/// the configuration file will be ignored.////// Configuration files are loaded after the configuration they're defined/// within in the order they're defined. **THIS IS A VERY SUBTLE BUT IMPORTANT/// POINT.** To put it another way: configuration files do not take effect/// until after the entire configuration is loaded. For example, in the/// configuration below:////// ```/// config-file = "foo"/// a = 1/// ```////// If "foo" contains `a = 2`, the final value of `a` will be 2, because/// `foo` is loaded after the configuration file that configures the/// nested `config-file` value.@"config-file": RepeatablePath = .{},
/// When this is true, the default configuration file paths will be loaded./// The default configuration file paths are currently only the XDG/// config path ($XDG_CONFIG_HOME/ghostty/config.ghostty).////// If this is false, the default configuration paths will not be loaded./// This is targeted directly at using Ghostty from the CLI in a way/// that minimizes external effects.////// This is a CLI-only configuration. Setting this in a configuration file/// will have no effect. It is not an error, but it will not do anything./// This configuration can only be set via CLI arguments.@"config-default-files": bool = true,
/// Confirms that a surface should be closed before closing it.////// This defaults to `true`. If set to `false`, surfaces will close without/// any confirmation. This can also be set to `always`, which will always/// confirm closing a surface, even if shell integration says a process isn't/// running.@"confirm-close-surface": ConfirmCloseSurface = .true,
/// Whether or not to quit after the last surface is closed.////// This defaults to `false` on macOS since that is standard behavior for/// a macOS application. On Linux, this defaults to `true` since that is/// generally expected behavior.////// On Linux, if this is `true`, Ghostty can delay quitting fully until a/// configurable amount of time has passed after the last window is closed./// See the documentation of `quit-after-last-window-closed-delay`.@"quit-after-last-window-closed": bool = builtin.os.tag == .linux,
/// Controls how long Ghostty will stay running after the last open surface has/// been closed. This only has an effect if `quit-after-last-window-closed` is/// also set to `true`.////// The minimum value for this configuration is `1s`. Any values lower than/// this will be clamped to `1s`.////// The duration is specified as a series of numbers followed by time units./// Whitespace is allowed between numbers and units. Each number and unit will/// be added together to form the total duration.////// The allowed time units are as follows:////// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments/// are made for leap years or leap seconds./// * `d` - one SI day, or 86400 seconds./// * `h` - one hour, or 3600 seconds./// * `m` - one minute, or 60 seconds./// * `s` - one second./// * `ms` - one millisecond, or 0.001 second./// * `us` or `µs` - one microsecond, or 0.000001 second./// * `ns` - one nanosecond, or 0.000000001 second.////// Examples:/// * `1h30m`/// * `45s`////// Units can be repeated and will be added together. This means that/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided./// A future update may disallow this.////// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any/// value larger than this will be clamped to the maximum value.////// By default `quit-after-last-window-closed-delay` is unset and/// Ghostty will quit immediately after the last window is closed if/// `quit-after-last-window-closed` is `true`.////// Only implemented on Linux.@"quit-after-last-window-closed-delay": ?Duration = null,
/// This controls whether an initial window is created when Ghostty/// is run. Note that if `quit-after-last-window-closed` is `true` and/// `quit-after-last-window-closed-delay` is set, setting `initial-window` to/// `false` will mean that Ghostty will quit after the configured delay if no/// window is ever created. Only implemented on Linux and macOS.@"initial-window": bool = true,
/// The duration that undo operations remain available. After this/// time, the operation will be removed from the undo stack and/// cannot be undone.////// The default value is 5 seconds.////// This timeout applies per operation, meaning that if you perform/// multiple operations, each operation will have its own timeout./// New operations do not reset the timeout of previous operations.////// A timeout of zero will effectively disable undo operations. It is/// not possible to set an infinite timeout, but you can set a very/// large timeout to effectively disable the timeout (on the order of years)./// This is highly discouraged, as it will cause the undo stack to grow/// indefinitely, memory usage to grow unbounded, and terminal sessions/// to never actually quit.////// The duration is specified as a series of numbers followed by time units./// Whitespace is allowed between numbers and units. Each number and unit will/// be added together to form the total duration.////// The allowed time units are as follows:////// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments/// are made for leap years or leap seconds./// * `d` - one SI day, or 86400 seconds./// * `h` - one hour, or 3600 seconds./// * `m` - one minute, or 60 seconds./// * `s` - one second./// * `ms` - one millisecond, or 0.001 second./// * `us` or `µs` - one microsecond, or 0.000001 second./// * `ns` - one nanosecond, or 0.000000001 second.////// Examples:/// * `1h30m`/// * `45s`////// Units can be repeated and will be added together. This means that/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided./// A future update may disallow this.////// This configuration is only supported on macOS. Linux doesn't/// support undo operations at all so this configuration has no/// effect.////// Available since: 1.2.0@"undo-timeout": Duration = .{ .duration = 5 * std.time.ns_per_s },
/// The position of the "quick" terminal window. To learn more about the/// quick terminal, see the documentation for the `toggle_quick_terminal`/// binding action.////// Valid values are:////// * `top` - Terminal appears at the top of the screen./// * `bottom` - Terminal appears at the bottom of the screen./// * `left` - Terminal appears at the left of the screen./// * `right` - Terminal appears at the right of the screen./// * `center` - Terminal appears at the center of the screen.////// On macOS, changing this configuration requires restarting Ghostty/// completely.////// Note: There is no default keybind for toggling the quick terminal./// To enable this feature, bind the `toggle_quick_terminal` action to a key.@"quick-terminal-position": QuickTerminalPosition = .top,
/// The size of the quick terminal.////// The size can be specified either as a percentage of the screen dimensions/// (height/width), or as an absolute size in pixels. Percentage values are/// suffixed with `%` (e.g. `20%`) while pixel values are suffixed with `px`/// (e.g. `300px`). A bare value without a suffix is a config error.////// When only one size is specified, the size parameter affects the size of/// the quick terminal on its *primary axis*, which depends on its position:/// height for quick terminals placed on the top or bottom, and width for left/// or right. The primary axis of a centered quick terminal depends on the/// monitor's orientation: height when on a landscape monitor, and width when/// on a portrait monitor.////// The *secondary axis* would be maximized for non-center positioned/// quick terminals unless another size parameter is specified, separated/// from the first by a comma (`,`). Percentage and pixel sizes can be mixed/// together: for instance, a size of `50%,500px` for a top-positioned quick/// terminal would be half a screen tall, and 500 pixels wide.////// Available since: 1.2.0@"quick-terminal-size": QuickTerminalSize = .{},
/// The layer of the quick terminal window. The higher the layer,/// the more windows the quick terminal may conceal.////// Valid values are:////// * `overlay`////// The quick terminal appears in front of all windows.////// * `top` (default)////// The quick terminal appears in front of normal windows but behind/// fullscreen overlays like lock screens.////// * `bottom`////// The quick terminal appears behind normal windows but in front of/// wallpapers and other windows in the background layer.////// * `background`////// The quick terminal appears behind all windows.////// GTK Wayland only.////// Available since: 1.2.0@"gtk-quick-terminal-layer": QuickTerminalLayer = .top,/// The namespace for the quick terminal window.////// This is an identifier that is used by the Wayland compositor and/or/// scripts to determine the type of layer surfaces and to possibly apply/// unique effects.////// GTK Wayland only.////// Available since: 1.2.0@"gtk-quick-terminal-namespace": [:0]const u8 = "ghostty-quick-terminal",
/// The screen where the quick terminal should show up.////// Valid values are:////// * `main` - The screen that the operating system recommends as the main/// screen. On macOS, this is the screen that is currently receiving/// keyboard input. This screen is defined by the operating system and/// not chosen by Ghostty.////// * `mouse` - The screen that the mouse is currently hovered over.////// * `macos-menu-bar` - The screen that contains the macOS menu bar as/// set in the display settings on macOS. This is a bit confusing because/// every screen on macOS has a menu bar, but this is the screen that/// contains the primary menu bar.////// The default value is `main` because this is the recommended screen/// by the operating system.////// On macOS, `macos-menu-bar` uses the screen containing the menu bar./// On Linux/Wayland, `macos-menu-bar` is treated as equivalent to `main`.////// Note: On Linux, there is no universal concept of a "primary" monitor./// Ghostty uses the compositor-reported primary output when available and/// falls back to the first monitor reported by GDK if no primary output can/// be resolved.@"quick-terminal-screen": QuickTerminalScreen = .main,
/// Duration (in seconds) of the quick terminal enter and exit animation./// Set it to 0 to disable animation completely. This can be changed at/// runtime.////// Only implemented on macOS.@"quick-terminal-animation-duration": f64 = 0.2,
/// Automatically hide the quick terminal when focus shifts to another window./// Set it to false for the quick terminal to remain open even when it loses focus.////// Defaults to true on macOS and on false on Linux/BSD. This is because global/// shortcuts on Linux require system configuration and are considerably less/// accessible than on macOS, meaning that it is more preferable to keep the/// quick terminal open until the user has completed their task./// This default may change in the future.@"quick-terminal-autohide": bool = switch (builtin.os.tag) { .linux => false, .macos => true, else => false,},
/// This configuration option determines the behavior of the quick terminal/// when switching between macOS spaces. macOS spaces are virtual desktops/// that can be manually created or are automatically created when an/// application is in full-screen mode.////// Valid values are:////// * `move` - When switching to another space, the quick terminal will/// also moved to the current space.////// * `remain` - The quick terminal will stay only in the space where it/// was originally opened and will not follow when switching to another/// space.////// The default value is `move`.////// Only implemented on macOS./// On Linux the behavior is always equivalent to `move`.////// Available since: 1.1.0@"quick-terminal-space-behavior": QuickTerminalSpaceBehavior = .move,
/// Determines under which circumstances that the quick terminal should receive/// keyboard input. See the corresponding [Wayland documentation](https://wayland.app/protocols/wlr-layer-shell-unstable-v1#zwlr_layer_surface_v1:enum:keyboard_interactivity)/// for a more detailed explanation of the behavior of each option.////// > [!NOTE]/// > The exact behavior of each option may differ significantly across/// > compositors -- experiment with them on your system to find one that/// > suits your liking!////// Valid values are:////// * `none`////// The quick terminal will not receive any keyboard input.////// * `on-demand` (default)////// The quick terminal would only receive keyboard input when it is focused.////// * `exclusive`////// The quick terminal will always receive keyboard input, even when another/// window is currently focused.////// Only has an effect on Linux Wayland./// On macOS the behavior is always equivalent to `on-demand`.////// Available since: 1.2.0@"quick-terminal-keyboard-interactivity": QuickTerminalKeyboardInteractivity = .@"on-demand",
/// Whether to enable shell integration auto-injection or not. Shell integration/// greatly enhances the terminal experience by enabling a number of features:////// * Working directory reporting so new tabs, splits inherit the/// previous terminal's working directory.////// * Prompt marking that enables the "jump_to_prompt" keybinding.////// * If you're sitting at a prompt, closing a terminal will not ask/// for confirmation.////// * Resizing the window with a complex prompt usually paints much/// better.////// Allowable values are:////// * `none` - Do not do any automatic injection. You can still manually/// configure your shell to enable the integration.////// * `detect` - Detect the shell based on the filename.////// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell injection scheme.////// The default value is `detect`.@"shell-integration": ShellIntegration = .detect,
/// Shell integration features to enable. These require our shell integration/// to be loaded, either automatically via shell-integration or manually.////// The format of this is a list of features to enable separated by commas. If/// you prefix a feature with `no-` then it is disabled. If you omit a feature,/// its default value is used, so you must explicitly disable features you don't/// want. You can also use `true` or `false` to turn all features on or off.////// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title`////// Available features:////// * `cursor` - Set the cursor to a bar at the prompt.////// * `sudo` - Set sudo wrapper to preserve terminfo.////// * `title` - Set the window title via shell integration.////// * `ssh-env` - Enable SSH environment variable compatibility. Automatically/// converts TERM from `xterm-ghostty` to `xterm-256color` when connecting to/// remote hosts and propagates COLORTERM, TERM_PROGRAM, and TERM_PROGRAM_VERSION./// Whether or not these variables will be accepted by the remote host(s) will/// depend on whether or not the variables are allowed in their sshd_config./// (Available since: 1.2.0)////// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts./// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when/// connecting to hosts that lack it. Requires `infocmp` to be available locally/// and `tic` to be available on remote hosts. Once terminfo is installed on a/// remote host, it will be automatically "cached" to avoid repeat installations./// If desired, the `+ssh-cache` CLI action can be used to manage the installation/// cache manually using various arguments./// (Available since: 1.2.0)////// * `path` - Add Ghostty's binary directory to PATH. This ensures the `ghostty`/// command is available in the shell even if shell init scripts reset PATH./// This is particularly useful on macOS where PATH is often overridden by/// system scripts. The directory is only added if not already present.////// SSH features work independently and can be combined for optimal experience:/// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its/// terminfo on remote hosts and use `xterm-ghostty` as TERM, falling back to/// `xterm-256color` with environment variables if terminfo installation fails.@"shell-integration-features": ShellIntegrationFeatures = .{},
/// Custom entries into the command palette.////// Each entry requires the title, the corresponding action, and an optional/// description. Each field should be prefixed with the field name, a colon/// (`:`), and then the specified value. The syntax for actions is identical/// to the one for keybind actions. Whitespace in between fields is ignored.////// If you need to embed commas or any other special characters in the values,/// enclose the value in double quotes and it will be interpreted as a Zig/// string literal. This is also useful for including whitespace at the/// beginning or the end of a value. See the/// [Zig documentation](https://ziglang.org/documentation/master/#Escape-Sequences)/// for more information on string literals. Note that multiline string literals/// are not supported.////// Double quotes can not be used around the field names.////// ```ini/// command-palette-entry = title:Reset Font Style, action:csi:0m/// command-palette-entry = title:Crash on Main Thread,description:Causes a crash on the main (UI) thread.,action:crash:main/// command-palette-entry = title:Focus Split: Right,description:"Focus the split to the right, if it exists.",action:goto_split:right/// command-palette-entry = title:"Ghostty",description:"Add a little Ghostty to your terminal.",action:"text:\xf0\x9f\x91\xbb"/// ```////// There are some additional special values that can be specified for/// command-palette-entry:////// * `command-palette-entry=clear` will clear all command entries. Warning: this/// removes ALL entries up to this point, including the default/// entries. Available since: 1.4.0////// By default, the command palette is preloaded with most actions that might/// be useful in an interactive setting yet do not have easily accessible or/// memorizable shortcuts. The default entries can be restored by setting this/// setting to an empty value:////// ```ini/// command-palette-entry =/// ```////// Available since: 1.2.0@"command-palette-entry": RepeatableCommand = .{},
/// Sets the reporting format for OSC sequences that request color information./// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and/// OSC 4 (256 color palette) queries, and by default the reported values/// are scaled-up RGB values, where each component are 16 bits. This is how/// most terminals report these values. However, some legacy applications may/// require 8-bit, unscaled, components. We also support turning off reporting/// altogether. The components are lowercase hex values.////// Allowable values are:////// * `none` - OSC 4/10/11 queries receive no reply////// * `8-bit` - Color components are return unscaled, e.g. `rr/gg/bb`////// * `16-bit` - Color components are returned scaled, e.g. `rrrr/gggg/bbbb`////// The default value is `16-bit`.@"osc-color-report-format": OSCColorReportFormat = .@"16-bit",
/// If true, allows the "KAM" mode (ANSI mode 2) to be used within/// the terminal. KAM disables keyboard input at the request of the/// application. This is not a common feature and is not recommended/// to be enabled. This will not be documented further because/// if you know you need KAM, you know. If you don't know if you/// need KAM, you don't need it.@"vt-kam-allowed": bool = false,
/// Custom shaders to run after the default shaders. This is a file path/// to a GLSL-syntax shader for all platforms.////// Warning: Invalid shaders can cause Ghostty to become unusable such as by/// causing the window to be completely black. If this happens, you can/// unset this configuration to disable the shader.////// Custom shader support is based on and compatible with the Shadertoy shaders./// Shaders should specify a `mainImage` function and the available uniforms/// largely match Shadertoy, with some caveats and Ghostty-specific extensions.////// The uniform values available to shaders are as follows:////// * `sampler2D iChannel0` - Input texture.////// A texture containing the current terminal screen. If multiple custom/// shaders are specified, the output of previous shaders is written to/// this texture, to allow combining multiple effects.////// * `vec3 iResolution` - Output texture size, `[width, height, 1]` (in px).////// * `float iTime` - Time in seconds since first frame was rendered.////// * `float iTimeDelta` - Time in seconds since previous frame was rendered.////// * `float iFrameRate` - Average framerate. (NOT CURRENTLY SUPPORTED)////// * `int iFrame` - Number of frames that have been rendered so far.////// * `float iChannelTime[4]` - Current time for video or sound input. (N/A)////// * `vec3 iChannelResolution[4]` - Resolutions of the 4 input samplers.////// Currently only `iChannel0` exists, and `iChannelResolution[0]` is/// identical to `iResolution`.////// * `vec4 iMouse` - Mouse input info. (NOT CURRENTLY SUPPORTED)////// * `vec4 iDate` - Date/time info. (NOT CURRENTLY SUPPORTED)////// * `float iSampleRate` - Sample rate for audio. (N/A)////// Ghostty-specific extensions:////// * `vec4 iCurrentCursor` - Info about the terminal cursor.////// - `iCurrentCursor.xy` is the -X, +Y corner of the current cursor./// - `iCurrentCursor.zw` is the width and height of the current cursor.////// * `vec4 iPreviousCursor` - Info about the previous terminal cursor.////// * `vec4 iCurrentCursorColor` - Color of the terminal cursor.////// * `vec4 iPreviousCursorColor` - Color of the previous terminal cursor.////// * `vec4 iCurrentCursorStyle` - Style of the terminal cursor////// Macros simplified use are defined for the various cursor styles:////// - `CURSORSTYLE_BLOCK` or `0`/// - `CURSORSTYLE_BLOCK_HOLLOW` or `1`/// - `CURSORSTYLE_BAR` or `2`/// - `CURSORSTYLE_UNDERLINE` or `3`/// - `CURSORSTYLE_LOCK` or `4`////// * `vec4 iPreviousCursorStyle` - Style of the previous terminal cursor////// * `vec4 iCursorVisible` - Visibility of the terminal cursor.////// * `float iTimeCursorChange` - Timestamp of terminal cursor change.////// When the terminal cursor changes position or color, this is set to/// the same time as the `iTime` uniform, allowing you to compute the/// time since the change by subtracting this from `iTime`.////// * `float iTimeFocus` - Timestamp when the surface last gained iFocus.////// When the surface gains focus, this is set to the current value of/// `iTime`, similar to how `iTimeCursorChange` works. This allows you/// to compute the time since focus was gained or lost by calculating/// `iTime - iTimeFocus`. Use this to create animations that restart/// when the terminal regains focus.////// * `int iFocus` - Current focus state of the surface.////// Set to 1.0 when the surface is focused, 0.0 when unfocused. This/// allows shaders to detect unfocused state and avoid animation artifacts/// from large time deltas caused by infrequent "deceptive frames"/// (e.g., modifier key presses, link hover events in unfocused split panes)./// Check `iFocus > 0` to determine if the surface is currently focused.////// * `vec3 iPalette[256]` - The 256-color terminal palette.////// RGB values for all 256 colors in the terminal palette, normalized/// to [0.0, 1.0]. Index 0-15 are the ANSI colors, 16-231 are the 6x6x6/// color cube, and 232-255 are the grayscale colors.////// * `vec3 iBackgroundColor` - Terminal background color (RGB).////// * `vec3 iForegroundColor` - Terminal foreground color (RGB).////// * `vec3 iCursorColor` - Terminal cursor color (RGB).////// * `vec3 iCursorText` - Terminal cursor text color (RGB).////// * `vec3 iSelectionBackgroundColor` - Selection background color (RGB).////// * `vec3 iSelectionForegroundColor` - Selection foreground color (RGB).////// If the shader fails to compile, the shader will be ignored. Any errors/// related to shader compilation will not show up as configuration errors/// and only show up in the log, since shader compilation happens after/// configuration loading on the dedicated render thread. For interactive/// development, use [shadertoy.com](https://shadertoy.com).////// This can be repeated multiple times to load multiple shaders. The shaders/// will be run in the order they are specified.////// This can be changed at runtime and will affect all open terminals.@"custom-shader": RepeatablePath = .{},
/// If `true` (default), the focused terminal surface will run an animation/// loop when custom shaders are used. This uses slightly more CPU (generally/// less than 10%) but allows the shader to animate. This only runs if there/// are custom shaders and the terminal is focused.////// If this is set to `false`, the terminal and custom shader will only render/// when the terminal is updated. This is more efficient but the shader will/// not animate.////// This can also be set to `always`, which will always run the animation/// loop regardless of whether the terminal is focused or not. The animation/// loop will still only run when custom shaders are used. Note that this/// will use more CPU per terminal surface and can become quite expensive/// depending on the shader and your terminal usage.////// This can be changed at runtime and will affect all open terminals.@"custom-shader-animation": CustomShaderAnimation = .true,
/// Bell features to enable if bell support is available in your runtime. Not/// all features are available on all runtimes. The format of this is a list of/// features to enable separated by commas. If you prefix a feature with `no-`/// then it is disabled. If you omit a feature, its default value is used.////// Valid values are:////// * `system`////// Instruct the system to notify the user using built-in system functions./// This could result in an audiovisual effect, a notification, or something/// else entirely. Changing these effects require altering system settings:/// for instance under the "Sound > Alert Sound" setting in GNOME,/// or the "Accessibility > System Bell" settings in KDE Plasma.////// On macOS, this plays the system alert sound.////// * `audio`////// Play a custom sound. (Available since 1.3.0 on macOS)////// * `attention` *(enabled by default)*////// Request the user's attention when Ghostty is unfocused, until it has/// received focus again. On macOS, this will bounce the app icon in the/// dock once. On Linux, the behavior depends on the desktop environment/// and/or the window manager/compositor:////// - On KDE, the background of the desktop icon in the task bar would be/// highlighted;////// - On GNOME, you may receive a notification that, when clicked, would/// bring the Ghostty window into focus;////// - On Sway, the window may be decorated with a distinctly colored border;////// - On other systems this may have no effect at all.////// * `title` *(enabled by default)*////// Prepend a bell emoji (🔔) to the title of the alerted surface until the/// terminal is re-focused or interacted with (such as on keyboard input).////// * `border`////// Display a border around the alerted surface until the terminal is/// re-focused or interacted with (such as on keyboard input).////// Available since: 1.2.0 on GTK, 1.2.1 on macOS////// Example: `audio`, `no-audio`, `system`, `no-system`////// Available since: 1.2.0@"bell-features": BellFeatures = .{},
/// If `audio` is an enabled bell feature, this is a path to an audio file. If/// the path is not absolute, it is considered relative to the directory of the/// configuration file that it is referenced from, or from the current working/// directory if this is used as a CLI flag. The path may be prefixed with `~/`/// to reference the user's home directory.////// Available since: 1.2.0 on GTK, 1.3.0 on macOS.@"bell-audio-path": ?Path = null,
/// If `audio` is an enabled bell feature, this is the volume to play the audio/// file at (relative to the system volume). This is a floating point number/// ranging from 0.0 (silence) to 1.0 (as loud as possible). The default is 0.5.////// Available since: 1.2.0 on GTK, 1.3.0 on macOS.@"bell-audio-volume": f64 = 0.5,
/// Control the in-app notifications that Ghostty shows.////// On Linux (GTK), in-app notifications show up as toasts. Toasts appear/// overlaid on top of the terminal window. They are used to show information/// that is not critical but may be important.////// Possible notifications are:////// - `clipboard-copy` (default: true) - Show a notification when text is copied/// to the clipboard./// - `config-reload` (default: true) - Show a notification when/// the configuration is reloaded.////// To specify a notification to enable, specify the name of the notification./// To specify a notification to disable, prefix the name with `no-`. For/// example, to disable `clipboard-copy`, set this configuration to/// `no-clipboard-copy`. To enable it, set this configuration to `clipboard-copy`.////// Multiple notifications can be enabled or disabled by separating them/// with a comma.////// A value of "false" will disable all notifications. A value of "true" will/// enable all notifications.////// This configuration only applies to GTK.////// Available since: 1.1.0@"app-notifications": AppNotifications = .{},
/// If anything other than false, fullscreen mode on macOS will not use the/// native fullscreen, but make the window fullscreen without animations and/// using a new space. It's faster than the native fullscreen mode since it/// doesn't use animations.////// Important: tabs DO NOT WORK in this mode. Non-native fullscreen removes/// the titlebar and macOS native tabs require the titlebar. If you use tabs,/// you should not use this mode.////// If you fullscreen a window with tabs, the currently focused tab will/// become fullscreen while the others will remain in a separate window in/// the background. You can switch to that window using normal window-switching/// keybindings such as command+tilde. When you exit fullscreen, the window/// will return to the tabbed state it was in before.////// Allowable values are:////// * `true` - Use non-native macOS fullscreen, hide the menu bar/// * `false` - Use native macOS fullscreen/// * `visible-menu` - Use non-native macOS fullscreen, keep the menu bar/// visible/// * `padded-notch` - Use non-native macOS fullscreen, hide the menu bar,/// but ensure the window is not obscured by the notch on applicable/// devices. The area around the notch will remain transparent currently,/// but in the future we may fill it with the window background color.////// Changing this option at runtime works, but will only apply to the next/// time the window is made fullscreen. If a window is already fullscreen,/// it will retain the previous setting until fullscreen is exited.@"macos-non-native-fullscreen": NonNativeFullscreen = .false,
/// Whether the window buttons in the macOS titlebar are visible. The window/// buttons are the colored buttons in the upper left corner of most macOS apps,/// also known as the traffic lights, that allow you to close, miniaturize, and/// zoom the window.////// This setting has no effect when `window-decoration = none` or/// `macos-titlebar-style = hidden`, as the window buttons are always hidden in/// these modes.////// Valid values are:////// * `visible` - Show the window buttons./// * `hidden` - Hide the window buttons.////// The default value is `visible`.////// Changing this option at runtime only applies to new windows.////// Available since: 1.2.0@"macos-window-buttons": MacWindowButtons = .visible,
/// The style of the macOS titlebar. Available values are: "native",/// "transparent", "tabs", and "hidden".////// The "native" style uses the native macOS titlebar with zero customization./// The titlebar will match your window theme (see `window-theme`).////// The "transparent" style is the same as "native" but the titlebar will/// be transparent and allow your window background color to come through./// This makes a more seamless window appearance but looks a little less/// typical for a macOS application and may not work well with all themes.////// The "transparent" style will also update in real-time to dynamic/// changes to the window background color, e.g. via OSC 11. To make this/// more aesthetically pleasing, this only happens if the terminal is/// a window, tab, or split that borders the top of the window. This/// avoids a disjointed appearance where the titlebar color changes/// but all the topmost terminals don't match.////// The "tabs" style is a completely custom titlebar that integrates the/// tab bar into the titlebar. This titlebar always matches the background/// color of the terminal. There are some limitations to this style:/// On macOS 13 and below, saved window state will not restore tabs correctly./// macOS 14 does not have this issue and any other macOS version has not/// been tested.////// The "hidden" style hides the titlebar. Unlike `window-decoration = none`,/// however, it does not remove the frame from the window or cause it to have/// squared corners. Changing to or from this option at run-time may affect/// existing windows in buggy ways.////// When "hidden", the top titlebar area can no longer be used for dragging/// the window. To drag the window, you can use option+click on the resizable/// areas of the frame to drag the window. This is a standard macOS behavior/// and not something Ghostty enables.////// The default value is "transparent". This is an opinionated choice/// but its one I think is the most aesthetically pleasing and works in/// most cases.////// Changing this option at runtime only applies to new windows.@"macos-titlebar-style": MacTitlebarStyle = .transparent,
/// Whether the proxy icon in the macOS titlebar is visible. The proxy icon/// is the icon that represents the folder of the current working directory./// You can see this very clearly in the macOS built-in Terminal.app/// titlebar.////// The proxy icon is only visible with the native macOS titlebar style.////// Valid values are:////// * `visible` - Show the proxy icon./// * `hidden` - Hide the proxy icon.////// The default value is `visible`.////// This setting can be changed at runtime and will affect all currently/// open windows but only after their working directory changes again./// Therefore, to make this work after changing the setting, you must/// usually `cd` to a different directory, open a different file in an/// editor, etc.@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible,
/// Controls the windowing behavior when dropping a file or folder/// onto the Ghostty icon in the macOS dock.////// Valid values are:////// * `new-tab` - Create a new tab in the current window, or open/// a new window if none exist./// * `new-window` - Create a new window unconditionally.////// The default value is `new-tab`.////// This setting is only supported on macOS and has no effect on other/// platforms.@"macos-dock-drop-behavior": MacOSDockDropBehavior = .@"new-tab",
/// macOS doesn't have a distinct "alt" key and instead has the "option"/// key which behaves slightly differently. On macOS by default, the/// option key plus a character will sometimes produce a Unicode character./// For example, on US standard layouts option-b produces "∫". This may be/// undesirable if you want to use "option" as an "alt" key for keybindings/// in terminal programs or shells.////// This configuration lets you change the behavior so that option is treated/// as alt.////// The default behavior (unset) will depend on your active keyboard/// layout. If your keyboard layout is one of the keyboard layouts listed/// below, then the default value is "true". Otherwise, the default/// value is "false". Keyboard layouts with a default value of "true" are:////// - U.S. Standard/// - U.S. International////// Note that if an *Option*-sequence doesn't produce a printable character, it/// will be treated as *Alt* regardless of this setting. (e.g. `alt+ctrl+a`).////// Explicit values that can be set:////// If `true`, the *Option* key will be treated as *Alt*. This makes terminal/// sequences expecting *Alt* to work properly, but will break Unicode input/// sequences on macOS if you use them via the *Alt* key.////// You may set this to `false` to restore the macOS *Alt* key unicode/// sequences but this will break terminal sequences expecting *Alt* to work.////// The values `left` or `right` enable this for the left or right *Option*/// key, respectively.@"macos-option-as-alt": ?inputpkg.OptionAsAlt = null,
/// Whether to enable the macOS window shadow. The default value is true./// With some window managers and window transparency settings, you may/// find false more visually appealing.@"macos-window-shadow": bool = true,
/// If true, the macOS icon in the dock and app switcher will be hidden. This is/// mainly intended for those primarily using the quick-terminal mode.////// Note that setting this to true means that keyboard layout changes/// will no longer be automatic.////// Control whether macOS app is excluded from the dock and app switcher,/// a "hidden" state. This is mainly intended for those primarily using/// quick-terminal mode, but is a general configuration for any use/// case.////// Available values:////// * `never` - The macOS app is never hidden./// * `always` - The macOS app is always hidden.////// Note: When the macOS application is hidden, keyboard layout changes/// will no longer be automatic. This is a limitation of macOS.////// Available since: 1.2.0@"macos-hidden": MacHidden = .never,
/// If true, Ghostty on macOS will automatically enable the "Secure Input"/// feature when it detects that a password prompt is being displayed.////// "Secure Input" is a macOS security feature that prevents applications from/// reading keyboard events. This can always be enabled manually using the/// `Ghostty > Secure Keyboard Entry` menu item.////// Note that automatic password prompt detection is based on heuristics/// and may not always work as expected. Specifically, it does not work/// over SSH connections, but there may be other cases where it also/// doesn't work.////// A reason to disable this feature is if you find that it is interfering/// with legitimate accessibility software (or software that uses the/// accessibility APIs), since secure input prevents any application from/// reading keyboard events.@"macos-auto-secure-input": bool = true,
/// If true, Ghostty will show a graphical indication when secure input is/// enabled. This indication is generally recommended to know when secure input/// is enabled.////// Normally, secure input is only active when a password prompt is displayed/// or it is manually (and typically temporarily) enabled. However, if you/// always have secure input enabled, the indication can be distracting and/// you may want to disable it.@"macos-secure-input-indication": bool = true,
/// If true, Ghostty exposes and handles the built-in AppleScript dictionary/// on macOS.////// If false, all AppleScript interactions are disabled. This includes/// AppleScript commands and AppleScript object lookup for windows, tabs,/// and terminals.////// The default is true.@"macos-applescript": bool = true,
/// Customize the macOS app icon.////// This only affects the icon that appears in the dock, application/// switcher, etc. This does not affect the icon in Finder because/// that is controlled by a hardcoded value in the signed application/// bundle and can't be changed at runtime. For more details on what/// exactly is affected, see the `NSApplication.icon` Apple documentation;/// that is the API that is being used to set the icon.////// Valid values:////// * `official` - Use the official Ghostty icon./// * `blueprint`, `chalkboard`, `microchip`, `glass`, `holographic`,/// `paper`, `retro`, `xray` - Official variants of the Ghostty icon/// hand-created by artists (no AI)./// * `custom` - Use a completely custom icon. The location must be specified/// using the additional `macos-custom-icon` configuration/// * `custom-style` - Use the official Ghostty icon but with custom/// styles applied to various layers. The custom styles must be/// specified using the additional `macos-icon`-prefixed configurations./// The `macos-icon-ghost-color` and `macos-icon-screen-color`/// configurations are required for this style.////// WARNING: The `custom-style` option is _experimental_. We may change/// the format of the custom styles in the future. We're still finalizing/// the exact layers and customization options that will be available.////// Other caveats:////// * The icon in the update dialog will always be the official icon./// This is because the update dialog is managed through a/// separate framework and cannot be customized without significant/// effort.@"macos-icon": MacAppIcon = .official,
/// The absolute path to the custom icon file./// Supported formats include PNG, JPEG, and ICNS.////// Defaults to `~/.config/ghostty/Ghostty.icns`@"macos-custom-icon": ?[:0]const u8 = null,
/// The material to use for the frame of the macOS app icon.////// Valid values:////// * `aluminum` - A brushed aluminum frame. This is the default./// * `beige` - A classic 90's computer beige frame./// * `plastic` - A glossy, dark plastic frame./// * `chrome` - A shiny chrome frame.////// Note: This configuration is required when `macos-icon` is set to/// `custom-style`.@"macos-icon-frame": MacAppIconFrame = .aluminum,
/// The color of the ghost in the macOS app icon.////// Note: This configuration is required when `macos-icon` is set to/// `custom-style`.////// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.@"macos-icon-ghost-color": ?Color = null,
/// The color of the screen in the macOS app icon.////// The screen is a linear gradient so you can specify multiple colors/// that make up the gradient. Up to 64 comma-separated colors may be/// specified as either hex (`#RRGGBB` or `RRGGBB`) or as named X11/// colors. The first color is the bottom of the gradient and the last/// color is the top of the gradient.////// Note: This configuration is required when `macos-icon` is set to/// `custom-style`.@"macos-icon-screen-color": ?ColorList = null,
/// Whether macOS Shortcuts are allowed to control Ghostty.////// Ghostty exposes a number of actions that allow Shortcuts to/// control and interact with Ghostty. This includes creating new/// terminals, sending text to terminals, running commands, invoking/// any keybind action, etc.////// This is a powerful feature but can be a security risk if a malicious/// shortcut is able to be installed and executed. Therefore, this/// configuration allows you to disable this feature.////// Valid values are:////// * `ask` - Ask the user whether for permission. Ghostty will remember/// this choice and never ask again. This is similar to other macOS/// permissions such as microphone access, camera access, etc.////// * `allow` - Allow Shortcuts to control Ghostty without asking.////// * `deny` - Deny Shortcuts from controlling Ghostty.////// Available since: 1.2.0@"macos-shortcuts": MacShortcuts = .ask,
/// Put every surface (tab, split, window) into a transient `systemd` scope.////// This allows per-surface resource management. For example, if a shell program/// is using too much memory, only that shell will be killed by the oom monitor/// instead of the entire Ghostty process. Similarly, if a shell program is/// using too much CPU, only that surface will be CPU-throttled.////// This will cause startup times to be slower (a hundred milliseconds or so),/// so the default value is "single-instance." In single-instance mode, only/// one instance of Ghostty is running (see gtk-single-instance) so the startup/// time is a one-time cost. Additionally, single instance Ghostty is much/// more likely to have many windows, tabs, etc. so cgroup isolation is a/// big benefit.////// This feature requires `systemd`. If `systemd` is unavailable, cgroup/// initialization will fail. By default, this will not prevent Ghostty from/// working (see `linux-cgroup-hard-fail`).////// Changing this value and reloading the config will not affect existing/// surfaces.////// Valid values are:////// * `never` - Never use cgroups./// * `always` - Always use cgroups./// * `single-instance` - Enable cgroups only for Ghostty instances launched/// as single-instance applications (see gtk-single-instance).@"linux-cgroup": LinuxCgroup = if (builtin.os.tag == .linux) .@"single-instance"else .never,
/// Memory limit for any individual terminal process (tab, split, window,/// etc.) in bytes. If this is unset then no memory limit will be set.////// Note that this sets the `MemoryHigh` setting on the transient `systemd`/// scope, which is a soft limit. You should configure something like/// `systemd-oom` to handle killing processes that have too much memory/// pressure.////// Changing this value and reloading the config will not affect existing/// surfaces.////// See the `systemd.resource-control` manual page for more information:/// https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html@"linux-cgroup-memory-limit": ?u64 = null,
/// Number of processes limit for any individual terminal process (tab, split,/// window, etc.). If this is unset then no limit will be set.////// Note that this sets the `TasksMax` setting on the transient `systemd` scope,/// which is a hard limit.////// Changing this value and reloading the config will not affect existing/// surfaces.////// See the `systemd.resource-control` manual page for more information:/// https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html@"linux-cgroup-processes-limit": ?u64 = null,
/// If this is false, then creating a transient `systemd` scope (for/// `linux-cgroup`) will be allowed to fail and the failure is ignored. This is/// useful if you view cgroup isolation as a "nice to have" and not a critical/// resource management feature, because surface creation will not fail if/// `systemd` APIs fail.////// If this is true, then any transient `systemd` scope creation failure will/// cause surface creation to fail.////// Changing this value and reloading the config will not affect existing/// surfaces.@"linux-cgroup-hard-fail": bool = false,
/// Enable or disable GTK's OpenGL debugging logs. The default is `true` for/// debug builds, `false` for all others.////// Available since: 1.1.0@"gtk-opengl-debug": bool = builtin.mode == .Debug,
/// If `true`, the Ghostty GTK application will run in single-instance mode:/// each new `ghostty` process launched will result in a new window if there is/// already a running process.////// If `false`, each new ghostty process will launch a separate application.////// If `detect`, Ghostty will assume true (single instance) unless one of/// the following scenarios is found:////// 1. TERM_PROGRAM environment variable is a non-empty value. In this/// case, we assume Ghostty is being launched from a graphical terminal/// session and you want a dedicated instance.////// 2. Any CLI arguments exist. In this case, we assume you are passing/// custom Ghostty configuration. Single instance mode inherits the/// configuration from when it was launched, so we must disable single/// instance to load the new configuration.////// If either of these scenarios is producing a false positive, you can/// set this configuration explicitly to the behavior you want.////// The pre-1.2 option `desktop` has been deprecated. Please replace/// this with `detect`.////// The default value is `detect`.////// Note that debug builds of Ghostty have a separate single-instance ID/// so you can test single instance without conflicting with release builds.@"gtk-single-instance": GtkSingleInstance = .default,
/// When enabled, the full GTK titlebar is displayed instead of your window/// manager's simple titlebar. The behavior of this option will vary with your/// window manager.////// This option does nothing when `window-decoration` is none or when running/// under macOS.@"gtk-titlebar": bool = true,
/// Determines the side of the screen that the GTK tab bar will stick to./// Top, bottom, and hidden are supported. The default is top.////// When `hidden` is set, a tab button displaying the number of tabs will appear/// in the title bar. It has the ability to open a tab overview for displaying/// tabs. Alternatively, you can use the `toggle_tab_overview` action in a/// keybind if your window doesn't have a title bar, or you can switch tabs/// with keybinds.@"gtk-tabs-location": GtkTabsLocation = .top,
/// If this is `true`, the titlebar will be hidden when the window is maximized,/// and shown when the titlebar is unmaximized. GTK only.////// Available since: 1.1.0@"gtk-titlebar-hide-when-maximized": bool = false,
/// Determines the appearance of the top and bottom bars tab bar.////// Valid values are:////// * `flat` - Top and bottom bars are flat with the terminal window./// * `raised` - Top and bottom bars cast a shadow on the terminal area./// * `raised-border` - Similar to `raised` but the shadow is replaced with a/// more subtle border.@"gtk-toolbar-style": GtkToolbarStyle = .raised,
/// The style of the GTK titlebar. Available values are `native` and `tabs`.////// The `native` titlebar style is a traditional titlebar with a title, a few/// buttons and window controls. A separate tab bar will show up below the/// titlebar if you have multiple tabs open in the window.////// The `tabs` titlebar merges the tab bar and the traditional titlebar./// This frees up vertical space on your screen if you use multiple tabs. One/// limitation of the `tabs` titlebar is that you cannot drag the titlebar/// by the titles any longer (as they are tab titles now). Other areas of the/// `tabs` title bar can be used to drag the window around.////// The default style is `native`.@"gtk-titlebar-style": GtkTitlebarStyle = .native,
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs/// are the new typical Gnome style where tabs fill their available space./// If you set this to `false` then tabs will only take up space they need,/// which is the old style.@"gtk-wide-tabs": bool = true,
/// Custom CSS files to be loaded.////// GTK CSS documentation can be found at the following links:////// * https://docs.gtk.org/gtk4/css-overview.html - An overview of GTK CSS./// * https://docs.gtk.org/gtk4/css-properties.html - A comprehensive list/// of supported CSS properties.////// Launch Ghostty with `env GTK_DEBUG=interactive ghostty` to tweak Ghostty's/// CSS in real time using the GTK Inspector. Errors in your CSS files would/// also be reported in the terminal you started Ghostty from. See/// https://developer.gnome.org/documentation/tools/inspector.html for more/// information about the GTK Inspector.////// This configuration can be repeated multiple times to load multiple files./// Prepend a ? character to the file path to suppress errors if the file does/// not exist. If you want to include a file that begins with a literal ?/// character, surround the file path in double quotes (")./// The file size limit for a single stylesheet is 5MiB.////// Available since: 1.1.0@"gtk-custom-css": RepeatablePath = .{},
/// If `true` (default), applications running in the terminal can show desktop/// notifications using certain escape sequences such as OSC 9 or OSC 777.@"desktop-notifications": bool = true,
/// If `true` (default), applications running in the terminal can show/// graphical progress bars using the ConEmu OSC 9;4 escape sequence./// If `false`, progress bar sequences are silently ignored.@"progress-style": bool = true,
/// Modifies the color used for bold text in the terminal.////// This can be set to a specific color, using the same format as/// `background` or `foreground` (e.g. `#RRGGBB` but other formats/// are also supported; see the aforementioned documentation). If a/// specific color is set, this color will always be used for the default/// bold text color. It will set the rest of the bold colors to `bright`.////// This can also be set to `bright`, which uses the bright color palette/// for bold text. For example, if the text is red, then the bold will/// use the bright red color. The terminal palette is set with `palette`/// but can also be overridden by the terminal application itself using/// escape sequences such as OSC 4. (Since Ghostty 1.2.0, the previous/// configuration `bold-is-bright` is deprecated and replaced by this/// usage).////// Available since Ghostty 1.2.0.@"bold-color": ?BoldColor = null,
/// The opacity level (opposite of transparency) of the faint text. A value of/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0/// or greater than 1 will be clamped to the nearest valid value.////// Available since Ghostty 1.2.0.@"faint-opacity": f64 = 0.5,
/// This will be used to set the `TERM` environment variable./// HACK: We set this with an `xterm` prefix because vim uses that to enable key/// protocols (specifically this will enable `modifyOtherKeys`), among other/// features. An option exists in vim to modify this: `:set/// keyprotocol=ghostty:kitty`, however a bug in the implementation prevents it/// from working properly. https://github.com/vim/vim/pull/13211 fixes this.term: []const u8 = "xterm-ghostty",
/// String to send when we receive `ENQ` (`0x05`) from the command that we are/// running. Defaults to an empty string if not set.@"enquiry-response": []const u8 = "",
/// Configures the low-level API to use for async IO, eventing, etc.////// Most users should leave this set to `auto`. This will automatically detect/// scenarios where APIs may not be available (for example `io_uring` on/// certain hardened kernels) and fall back to a different API. However, if/// you want to force a specific backend for any reason, you can set this/// here.////// Based on various benchmarks, we haven't found a statistically significant/// difference between the backends with regards to memory, CPU, or latency./// The choice of backend is more about compatibility and features.////// Available options:////// * `auto` - Automatically choose the best backend for the platform/// based on available options./// * `epoll` - Use the `epoll` API/// * `io_uring` - Use the `io_uring` API////// If the selected backend is not available on the platform, Ghostty will/// fall back to an automatically chosen backend that is available.////// Changing this value requires a full application restart to take effect.////// This is only supported on Linux, since this is the only platform/// where we have multiple options. On macOS, we always use `kqueue`.////// Available since: 1.2.0@"async-backend": AsyncBackend = .auto,
/// Control the auto-update functionality of Ghostty. This is only supported/// on macOS currently, since Linux builds are distributed via package/// managers that are not centrally controlled by Ghostty.////// Checking or downloading an update does not send any information to/// the project beyond standard network information mandated by the/// underlying protocols. To put it another way: Ghostty doesn't explicitly/// add any tracking to the update process. The update process works by/// downloading information about the latest version and comparing it/// client-side to the current version.////// Valid values are:////// * `off` - Disable auto-updates./// * `check` - Check for updates and notify the user if an update is/// available, but do not automatically download or install the update./// * `download` - Check for updates, automatically download the update,/// notify the user, but do not automatically install the update.////// If unset, we defer to Sparkle's default behavior, which respects the/// preference stored in the standard user defaults (`defaults(1)`).////// Changing this value at runtime works after a small delay.@"auto-update": ?AutoUpdate = null,
/// The release channel to use for auto-updates.////// The default value of this matches the release channel of the currently/// running Ghostty version. If you download a pre-release version of Ghostty/// then this will be set to `tip` and you will receive pre-release updates./// If you download a stable version of Ghostty then this will be set to/// `stable` and you will receive stable updates.////// Valid values are:////// * `stable` - Stable, tagged releases such as "1.0.0"./// * `tip` - Pre-release versions generated from each commit to the/// main branch. This is the version that was in use during private/// beta testing by thousands of people. It is generally stable but/// will likely have more bugs than the stable channel.////// Changing this configuration requires a full restart of/// Ghostty to take effect.////// This only works on macOS since only macOS has an auto-update feature.@"auto-update-channel": ?build_config.ReleaseChannel = null,
/// This is set by the CLI parser for deinit._arena: ?ArenaAllocator = null,
/// List of diagnostics that were generated during the loading of/// the configuration._diagnostics: cli.DiagnosticList = .{},
/// The conditional truths for the configuration. This is used to/// determine if a conditional configuration matches or not._conditional_state: conditional.State = .{},
/// The conditional keys that are used at any point during the configuration/// loading. This is used to speed up the conditional evaluation process._conditional_set: std.EnumSet(conditional.Key) = .{},
/// The steps we can use to reload the configuration after it has been loaded/// without reopening the files. This is used in very specific cases such/// as loadTheme which has more details on why._replay_steps: std.ArrayListUnmanaged(Replay.Step) = .{},
/// Set to true if Ghostty was executed as xdg-terminal-exec on Linux.@"_xdg-terminal-exec": bool = false,
pub fn deinit(self: *Config) void { if (self._arena) |arena| arena.deinit(); self.* = undefined;}
/// Load the configuration according to the default rules:////// 1. Defaults/// 2. XDG config dir/// 3. "Application Support" directory (macOS only)/// 4. CLI flags/// 5. Recursively defined configuration files///pub fn load(alloc_gpa: Allocator) !Config { var result = try default(alloc_gpa); errdefer result.deinit();
// If we have a configuration file in our home directory, parse that first. try result.loadDefaultFiles(alloc_gpa);
// Parse the config from the CLI args. try result.loadCliArgs(alloc_gpa);
// Parse the config files that were added from our file and CLI args. try result.loadRecursiveFiles(alloc_gpa); try result.finalize();
return result;}
pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { // Build up our basic config var result: Config = .{ ._arena = .init(alloc_gpa), }; errdefer result.deinit(); const alloc = result._arena.?.allocator();
// Add our default keybindings try result.keybind.init(alloc);
// Add our default command palette entries try result.@"command-palette-entry".init(alloc);
// Add our default link for URL detection try result.link.links.append(alloc, .{ .regex = url.regex, .action = .{ .open = {} }, .highlight = .{ .hover_mods = inputpkg.ctrlOrSuper(.{}) }, });
return result;}
/// Load configuration from an iterator that yields values that look like/// command-line arguments, i.e. `--key=value`.pub fn loadIter( self: *Config, alloc: Allocator, iter: anytype,) !void { try cli.args.parse(Config, alloc, self, iter);}
/// Load configuration from the target config file at `path`.////// `path` must be resolved and absolute.pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { assert(std.fs.path.isAbsolute(path)); var file = file_load.open(path) catch |err| switch (err) { error.NotAFile => { log.warn( "config-file {s}: not reading because it is not a file", .{path}, ); return; },
else => return err, }; defer file.close();
try self.loadFsFile(alloc, &file, path);}
/// Load config from the given File.fn loadFsFile(self: *Config, alloc: Allocator, file: *std.fs.File, path: []const u8) !void { std.log.info("reading configuration file path={s}", .{path}); var buf: [2048]u8 = undefined; var file_reader = file.reader(&buf); const reader = &file_reader.interface; try self.loadReader(alloc, reader, path);}
/// Load config from the given Reader.fn loadReader(self: *Config, alloc: Allocator, reader: *std.Io.Reader, path: []const u8) !void { bom: { // If the file starts with a UTF-8 byte order mark, skip it. // https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 const bom: []const u8 = &.{ 0xef, 0xbb, 0xbf }; const str = reader.peek(bom.len) catch break :bom; if (std.mem.eql(u8, str, bom)) { log.info("skipping UTF-8 byte order mark", .{}); reader.toss(bom.len); } } var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path }; try self.loadIter(alloc, &iter); try self.expandPaths(std.fs.path.dirname(path).?);}
test "handle bom in config files" { const testing = std.testing; const alloc = testing.allocator;
{ const data = "\xef\xbb\xbfabnormal-command-exit-runtime = 2500\n"; var reader: std.Io.Reader = .fixed(data); var cfg = try Config.default(alloc); defer cfg.deinit(); try cfg.loadReader( alloc, &reader, "/home/ghostty/.config/ghostty/config.ghostty", ); try cfg.finalize();
try testing.expect(cfg._diagnostics.empty()); try testing.expectEqual( 2500, cfg.@"abnormal-command-exit-runtime", ); }
{ const data = "abnormal-command-exit-runtime = 2500\n"; var reader: std.Io.Reader = .fixed(data); var cfg = try Config.default(alloc); defer cfg.deinit(); try cfg.loadReader( alloc, &reader, "/home/ghostty/.config/ghostty/config.ghostty", ); try cfg.finalize();
try testing.expect(cfg._diagnostics.empty()); try testing.expectEqual( 2500, cfg.@"abnormal-command-exit-runtime", ); }}
pub const OptionalFileAction = enum { loaded, not_found, @"error" };
/// Load optional configuration file from `path`. All errors are ignored.////// Returns the action that was taken.pub fn loadOptionalFile( self: *Config, alloc: Allocator, path: []const u8,) OptionalFileAction { if (self.loadFile(alloc, path)) { return .loaded; } else |err| switch (err) { error.FileNotFound => return .not_found, else => { std.log.warn( "error reading optional config file, not loading err={} path={s}", .{ err, path }, );
return .@"error"; }, }}
fn writeConfigTemplate(path: []const u8) !void { log.info("creating template config file: path={s}", .{path}); if (std.fs.path.dirname(path)) |dir_path| { try std.fs.cwd().makePath(dir_path); } const file = try std.fs.createFileAbsolute(path, .{}); defer file.close(); var buf: [4096]u8 = undefined; var file_writer = file.writer(&buf); const writer = &file_writer.interface; try writer.print( @embedFile("./config-template"), .{ .path = path }, );}
/// Load configurations from the default configuration files. The default/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config.ghostty`.////// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/`/// is also loaded.////// The legacy `config` file (without extension) is first loaded,/// then `config.ghostty`.pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { // Load XDG first const legacy_xdg_path = try file_load.legacyDefaultXdgPath(alloc); defer alloc.free(legacy_xdg_path); const xdg_path = try file_load.defaultXdgPath(alloc); defer alloc.free(xdg_path); const xdg_loaded: bool = xdg_loaded: { const legacy_xdg_action = self.loadOptionalFile(alloc, legacy_xdg_path); const xdg_action = self.loadOptionalFile(alloc, xdg_path); if (xdg_action != .not_found and legacy_xdg_action != .not_found) { log.warn("both config files `{s}` and `{s}` exist.", .{ legacy_xdg_path, xdg_path }); log.warn("loading them both in that order", .{}); break :xdg_loaded true; }
break :xdg_loaded xdg_action != .not_found or legacy_xdg_action != .not_found; };
// On macOS load the app support directory as well if (comptime builtin.os.tag == .macos) { const legacy_app_support_path = try file_load.legacyDefaultAppSupportPath(alloc); defer alloc.free(legacy_app_support_path); const app_support_path = try file_load.preferredAppSupportPath(alloc); defer alloc.free(app_support_path); const app_support_loaded: bool = loaded: { const legacy_app_support_action = self.loadOptionalFile( alloc, legacy_app_support_path, );
// The app support path and legacy may be the same, since we // use the `preferred` call above. If its the same, avoid // a double-load. const app_support_action: OptionalFileAction = if (!std.mem.eql( u8, legacy_app_support_path, app_support_path, )) self.loadOptionalFile( alloc, app_support_path, ) else .not_found;
if (app_support_action != .not_found and legacy_app_support_action != .not_found) { log.warn( "both config files `{s}` and `{s}` exist.", .{ legacy_app_support_path, app_support_path }, ); log.warn("loading them both in that order", .{}); break :loaded true; }
break :loaded app_support_action != .not_found or legacy_app_support_action != .not_found; };
// If both files are not found, then we create a template file. // For macOS, we only create the template file in the app support if (!app_support_loaded and !xdg_loaded) { writeConfigTemplate(app_support_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; } } else { if (!xdg_loaded) { writeConfigTemplate(xdg_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; } }}
/// Load and parse the CLI args.pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { switch (builtin.os.tag) { .windows => {},
// Fast-path if we are Linux/BSD and have no args. .linux, .freebsd => if (std.os.argv.len <= 1) return,
// Everything else we have to at least try because it may // not use std.os.argv. else => {}, }
// On Linux, we have a special case where if the executing // program is "xdg-terminal-exec" then we treat all CLI // args as if they are a command to execute. // // In this mode, we also behave slightly differently: // // - The initial window title is set to the full command. This // can be used with window managers to modify positioning, // styling, etc. based on the command. // // See: https://github.com/Vladimir-csp/xdg-terminal-exec if ((comptime builtin.os.tag == .linux) or (comptime builtin.os.tag == .freebsd)) { if (internal_os.xdg.parseTerminalExec(std.os.argv)) |args| { const arena_alloc = self._arena.?.allocator();
// First, we add an artificial "-e" so that if we // replay the inputs to rebuild the config (i.e. if // a theme is set) then we will get the same behavior. try self._replay_steps.append(arena_alloc, .@"-e");
// Next, take all remaining args and use that to build up // a command to execute. var builder: std.ArrayList([:0]const u8) = .empty; errdefer builder.deinit(arena_alloc); for (args) |arg_raw| { const arg = std.mem.sliceTo(arg_raw, 0); const copy = try arena_alloc.dupeZ(u8, arg); try self._replay_steps.append(arena_alloc, .{ .arg = copy }); try builder.append(arena_alloc, copy); }
self.@"_xdg-terminal-exec" = true; self.@"initial-command" = .{ .direct = try builder.toOwnedSlice(arena_alloc) }; return; } }
// We set config-default-files to true here because this // should always be reset so we can detect if it is set // in the CLI since it is documented as having no affect // from files. self.@"config-default-files" = true;
// Keep track of the replay steps up to this point so we // can replay if we are discarding the default files. const replay_len_start = self._replay_steps.items.len;
// font-family settings set via the CLI overwrite any prior values // rather than append. This avoids a UX oddity where you have to // specify `font-family=""` to clear the font families. const fields = &[_][]const u8{ "font-family", "font-family-bold", "font-family-italic", "font-family-bold-italic", }; inline for (fields) |field| @field(self, field).overwrite_next = true; defer { inline for (fields) |field| @field(self, field).overwrite_next = false; }
// Initialize our CLI iterator. var iter = try cli.args.argsIterator(alloc_gpa); defer iter.deinit(); try self.loadIter(alloc_gpa, &iter);
// If we are not loading the default files, then we need to // replay the steps up to this point so that we can rebuild // the config without it. if (!self.@"config-default-files") reload: { const replay_len_end = self._replay_steps.items.len; if (replay_len_end == replay_len_start) break :reload; log.info("config-default-files unset, discarding configuration from default files", .{});
var new_config = try self.cloneEmpty(alloc_gpa); errdefer new_config.deinit(); var it = Replay.iterator( self._replay_steps.items[replay_len_start..replay_len_end], &new_config, ); try new_config.loadIter(alloc_gpa, &it); self.deinit(); self.* = new_config; }
// Any paths referenced from the CLI are relative to the current working // directory. var buf: [std.fs.max_path_bytes]u8 = undefined; try self.expandPaths(try std.fs.cwd().realpath(".", &buf));}
/// Load and parse the config files that were added in the "config-file" key.pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { if (self.@"config-file".value.items.len == 0) return; const arena_alloc = self._arena.?.allocator();
// Keeps track of loaded files to prevent cycles. var loaded = std.StringHashMap(void).init(alloc_gpa); defer loaded.deinit();
// We need to insert all of our loaded config-file values // PRIOR to the "-e" in our replay steps, since everything // after "-e" becomes an "initial-command". To do this, we // dupe the values if we find it. var replay_suffix: std.ArrayList(Replay.Step) = .empty; defer replay_suffix.deinit(alloc_gpa); for (self._replay_steps.items, 0..) |step, i| if (step == .@"-e") { // We don't need to clone the steps because they should // all be allocated in our arena and we're keeping our // arena. try replay_suffix.appendSlice(alloc_gpa, self._replay_steps.items[i..]);
// Remove our old values. Again, don't need to free any // memory here because its all part of our arena. self._replay_steps.shrinkRetainingCapacity(i); break; };
// We must use a while below and not a for(items) because we // may add items to the list while iterating for recursive // config-file entries. var i: usize = 0; while (i < self.@"config-file".value.items.len) : (i += 1) { const path, const optional = switch (self.@"config-file".value.items[i]) { .optional => |path| .{ path, true }, .required => |path| .{ path, false }, };
// Error paths if (path.len == 0) continue;
// All paths should already be absolute at this point because // they're fixed up after each load. assert(std.fs.path.isAbsolute(path));
// We must only load a unique file once if (try loaded.fetchPut(path, {}) != null) { const diag: cli.Diagnostic = .{ .message = try std.fmt.allocPrintSentinel( arena_alloc, "config-file {s}: cycle detected", .{path}, 0, ), };
try self._diagnostics.append(arena_alloc, diag); try self._replay_steps.append(arena_alloc, .{ .diagnostic = diag }); continue; }
var file = std.fs.openFileAbsolute(path, .{}) catch |err| { if (err != error.FileNotFound or !optional) { const diag: cli.Diagnostic = .{ .message = try std.fmt.allocPrintSentinel( arena_alloc, "error opening config-file {s}: {}", .{ path, err }, 0, ), };
try self._diagnostics.append(arena_alloc, diag); try self._replay_steps.append(arena_alloc, .{ .diagnostic = diag }); } continue; }; defer file.close();
const stat = try file.stat(); switch (stat.kind) { .file => {}, else => |kind| { const diag: cli.Diagnostic = .{ .message = try std.fmt.allocPrintSentinel( arena_alloc, "config-file {s}: not reading because file type is {s}", .{ path, @tagName(kind) }, 0, ), };
try self._diagnostics.append(arena_alloc, diag); try self._replay_steps.append(arena_alloc, .{ .diagnostic = diag }); continue; }, }
try self.loadFsFile(arena_alloc, &file, path); }
// If we have a suffix, add that back. if (replay_suffix.items.len > 0) { try self._replay_steps.appendSlice( arena_alloc, replay_suffix.items, ); }}
/// Get the arena allocator associated with the configuration.pub fn arenaAlloc(self: *Config) Allocator { return self._arena.?.allocator();}
/// Change the state of conditionals and reload the configuration/// based on the new state. This returns a new configuration based/// on the new state. The caller must free the old configuration if they/// wish.////// This returns null if the conditional state would result in no changes/// to the configuration. In this case, the caller can continue to use/// the existing configuration or clone if they want a copy.////// This doesn't re-read any files, it just re-applies the same/// configuration with the new conditional state. Importantly, this means/// that if you change the conditional state and the user in the interim/// deleted a file that was referenced in the configuration, then the/// configuration can still be reloaded.pub fn changeConditionalState( self: *const Config, new: conditional.State,) !?Config { // If the conditional state between the old and new is the same, // then we don't need to do anything. relevant: { inline for (@typeInfo(conditional.Key).@"enum".fields) |field| { const key: conditional.Key = @field(conditional.Key, field.name);
// Conditional set contains the keys that this config uses. So we // only continue if we use this key. if (self._conditional_set.contains(key) and !deepEqual( @TypeOf(@field(self._conditional_state, field.name)), @field(self._conditional_state, field.name), @field(new, field.name), )) { break :relevant; } }
// If we got here, then we didn't find any differences between // the old and new conditional state that would affect the // configuration. return null; }
// Create our new configuration const alloc_gpa = self._arena.?.child_allocator; var new_config = try self.cloneEmpty(alloc_gpa); errdefer new_config.deinit();
// Set our conditional state so the replay below can use it new_config._conditional_state = new;
// Replay all of our steps to rebuild the configuration var it = Replay.iterator(self._replay_steps.items, &new_config); try new_config.loadIter(alloc_gpa, &it); try new_config.finalize();
return new_config;}
/// Expand the relative paths in config-files to be absolute paths/// relative to the base directory.fn expandPaths(self: *Config, base: []const u8) !void { const arena_alloc = self._arena.?.allocator();
// Keep track of this step for replays try self._replay_steps.append( arena_alloc, .{ .expand = try arena_alloc.dupe(u8, base) }, );
// Expand all of our paths inline for (@typeInfo(Config).@"struct".fields) |field| { switch (field.type) { RepeatablePath, Path => { try @field(self, field.name).expand( arena_alloc, base, &self._diagnostics, ); }, ?RepeatablePath, ?Path => { if (@field(self, field.name)) |*path| { try path.expand( arena_alloc, base, &self._diagnostics, ); } }, else => {}, } }}
fn loadTheme(self: *Config, theme: Theme) !void { // Load the correct theme depending on the conditional state. // Dark/light themes were programmed prior to conditional configuration // so when we introduce that we probably want to replace this. const name: []const u8 = switch (self._conditional_state.theme) { .light => theme.light, .dark => theme.dark, };
// Find our theme file and open it. See the open function for details. const themefile = (try themepkg.open( self._arena.?.allocator(), name, &self._diagnostics, )) orelse return; const path = themefile.path; const file = themefile.file; defer file.close();
// From this point onwards, we load the theme and do a bit of a dance // to achieve two separate goals: // // (1) We want the theme to be loaded and our existing config to // override the theme. So we need to load the theme and apply // our config on top of it. // // (2) We want to free existing memory that we aren't using anymore // as a result of reloading the configuration. // // Point 2 is strictly a result of aur approach to point 1, but it is // a nice property to have to limit memory bloat as much as possible.
// Load into a new configuration so that we can free the existing memory. const alloc_gpa = self._arena.?.child_allocator; var new_config = try self.cloneEmpty(alloc_gpa); errdefer new_config.deinit();
// Load our theme var buf: [2048]u8 = undefined; var file_reader = file.reader(&buf); const reader = &file_reader.interface; var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path }; try new_config.loadIter(alloc_gpa, &iter);
// Setup our replay to be conditional. conditional: for (new_config._replay_steps.items) |*item| { switch (item.*) { .expand, .diagnostic => {},
// If we see "-e" then we do NOT make the following arguments // conditional since they are supposed to be part of the // initial command. .@"-e" => break :conditional,
// Change our arg to be conditional on our theme. .arg => |v| { const alloc_arena = new_config._arena.?.allocator(); const conds = try alloc_arena.alloc(Conditional, 1); conds[0] = .{ .key = .theme, .op = .eq, .value = @tagName(self._conditional_state.theme), }; item.* = .{ .conditional_arg = .{ .conditions = conds, .arg = v, } }; },
.conditional_arg => |v| { const alloc_arena = new_config._arena.?.allocator(); const conds = try alloc_arena.alloc(Conditional, v.conditions.len + 1); conds[0] = .{ .key = .theme, .op = .eq, .value = @tagName(self._conditional_state.theme), }; @memcpy(conds[1..], v.conditions); item.* = .{ .conditional_arg = .{ .conditions = conds, .arg = v.arg, } }; }, } }
// Replay our previous inputs so that we can override values // from the theme. var slice_it = Replay.iterator(self._replay_steps.items, &new_config); try new_config.loadIter(alloc_gpa, &slice_it);
// Success, swap our new config in and free the old. self.deinit(); self.* = new_config;}
/// Call this once after you are done setting configuration. This/// is idempotent but will waste memory if called multiple times.pub fn finalize(self: *Config) !void { // We always load the theme first because it may set other fields // in our config. if (self.theme) |theme| { const different = !std.mem.eql(u8, theme.light, theme.dark);
// Warning: loadTheme will deinit our existing config and replace // it so all memory from self prior to this point will be freed. try self.loadTheme(theme);
// If we have different light vs dark mode themes, disable // window-theme = auto since that breaks it. if (different) { // This setting doesn't make sense with different light/dark themes // because it'll force the theme based on the Ghostty theme. if (self.@"window-theme" == .auto) self.@"window-theme" = .system;
// Mark that we use a conditional theme self._conditional_set.insert(.theme); } }
const alloc = self._arena.?.allocator();
// Used for a variety of defaults. See the function docs as well the // specific variable use sites for more details. const probable_cli = probableCliEnvironment();
// If we have a font-family set and don't set the others, default // the others to the font family. This way, if someone does // --font-family=foo, then we try to get the stylized versions of // "foo" as well. if (self.@"font-family".count() > 0) { const fields = &[_][]const u8{ "font-family-bold", "font-family-italic", "font-family-bold-italic", }; inline for (fields) |field| { if (@field(self, field).count() == 0) { @field(self, field) = try self.@"font-family".clone(alloc); } } }
// Prevent setting TERM to an empty string if (self.term.len == 0) { // HACK: See comment above at definition self.term = "xterm-ghostty"; }
// The default for the working directory depends on the system. var wd: WorkingDirectory = self.@"working-directory" orelse if (probable_cli) .inherit else .home;
// If we are missing either a command or home directory, we need // to look up defaults which is kind of expensive. We only do this // on desktop. if ((comptime !builtin.target.cpu.arch.isWasm()) and (comptime !builtin.is_test)) { if (self.command == null or wd == .home) command: { // First look up the command using the SHELL env var if needed. // We don't do this in flatpak because SHELL in Flatpak is always // set to /bin/sh. if (self.command) |cmd| log.info("shell src=config value={}", .{cmd}) else shell_env: { // Flatpak always gets its shell from outside the sandbox if (internal_os.isFlatpak()) break :shell_env;
// If we were launched from the desktop, our SHELL env var // will represent our SHELL at login time. We only want to // read from SHELL if we're in a probable CLI environment. if (!probable_cli) break :shell_env;
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| { log.info("default shell source=env value={s}", .{value});
const copy = try alloc.dupeZ(u8, value); self.command = .{ .shell = copy };
// If we don't need the working directory, then we can exit now. if (wd != .home) break :command; } else |_| {} }
switch (builtin.os.tag) { .windows => { if (self.command == null) { log.warn("no default shell found, will default to using cmd", .{}); self.command = .{ .shell = "cmd.exe" }; }
if (wd == .home) { var buf: [std.fs.max_path_bytes]u8 = undefined; if (try internal_os.home(&buf)) |home| { wd = .{ .path = try alloc.dupe(u8, home) }; } else { wd = .inherit; } } },
else => { // We need the passwd entry for the remainder const pw = try internal_os.passwd.get(alloc); if (self.command == null) { if (pw.shell) |sh| { log.info("default shell src=passwd value={s}", .{sh}); self.command = .{ .shell = sh }; } }
if (wd == .home) { if (pw.home) |home| { log.info("default working directory src=passwd value={s}", .{home}); wd = .{ .path = home }; } else { wd = .inherit; } }
if (self.command == null) { log.warn("no default shell found, will default to using sh", .{}); } }, } } } try wd.finalize(alloc); self.@"working-directory" = wd;
// Apprt-specific defaults switch (build_config.app_runtime) { .none => {}, .gtk => { switch (self.@"gtk-single-instance") { .true, .false => {},
// For detection, we assume single instance unless we're // in a CLI environment, then we disable single instance. .detect => self.@"gtk-single-instance" = if (probable_cli) .false else .true, } }, }
// Default our click interval if (self.@"click-repeat-interval" == 0 and (comptime !builtin.is_test)) { self.@"click-repeat-interval" = internal_os.clickInterval() orelse 500; }
// Clamp our mouse scroll multiplier self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".precision)); self.@"mouse-scroll-multiplier".discrete = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".discrete));
// Clamp our split opacity self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity"));
// Clamp our contrast self.@"minimum-contrast" = @min(21, @max(1, self.@"minimum-contrast"));
// Minimum window size if (self.@"window-width" > 0) self.@"window-width" = @max(10, self.@"window-width"); if (self.@"window-height" > 0) self.@"window-height" = @max(4, self.@"window-height");
// If URLs are disabled, cut off the first link. The first link is // always the URL matcher. if (!self.@"link-url") self.link.links.items = self.link.links.items[1..];
// We warn when the quit-after-last-window-closed-delay is set to a very // short value because it can cause Ghostty to quit before the first // window is even shown. if (self.@"quit-after-last-window-closed-delay") |duration| { if (duration.duration < 5 * std.time.ns_per_s) { log.warn( "quit-after-last-window-closed-delay is set to a very short value ({f}), which might cause problems", .{duration}, ); } }
// We can't set this as a struct default because our config is // loaded in environments where a build config isn't available. if (self.@"auto-update-channel" == null) { self.@"auto-update-channel" = build_config.release_channel; }
self.@"faint-opacity" = std.math.clamp(self.@"faint-opacity", 0.0, 1.0);
// Finalize key remapping set for efficient lookups self.@"key-remap".finalize();}
/// Callback for src/cli/args.zig to allow us to handle special cases/// like `--help` or `-e`. Returns "false" if the CLI parsing should halt.pub fn parseManuallyHook( self: *Config, alloc: Allocator, arg: []const u8, iter: anytype,) !bool { if (std.mem.eql(u8, arg, "-e")) { // Add the special -e marker. This prevents: // (1) config-file from adding args to the end (see #2908) // (2) dark/light theme from making this conditional try self._replay_steps.append(alloc, .@"-e");
// Build up the command. We don't clean this up because we take // ownership in our allocator. var command: std.ArrayList([:0]const u8) = .empty; errdefer command.deinit(alloc);
while (iter.next()) |param| { const copy = try alloc.dupeZ(u8, param); try self._replay_steps.append(alloc, .{ .arg = copy }); try command.append(alloc, copy); }
if (command.items.len == 0) { try self._diagnostics.append(alloc, .{ .location = try cli.Location.fromIter(iter, alloc), .message = try std.fmt.allocPrintSentinel( alloc, "missing command after {s}", .{arg}, 0, ), });
return false; }
// See "command" docs for the implied configurations and why. self.@"initial-command" = .{ .direct = command.items }; self.@"gtk-single-instance" = .false; self.@"quit-after-last-window-closed" = true; self.@"quit-after-last-window-closed-delay" = null; if (self.@"shell-integration" != .none) { self.@"shell-integration" = .detect; }
// Do not continue, we consumed everything. return false; }
// Keep track of our input args for replay try self._replay_steps.append( alloc, .{ .arg = try alloc.dupeZ(u8, arg) }, );
// If we didn't find a special case, continue parsing normally return true;}
fn compatGtkTabsLocation( self: *Config, alloc: Allocator, key: []const u8, value: ?[]const u8,) bool { _ = alloc; assert(std.mem.eql(u8, key, "gtk-tabs-location"));
if (std.mem.eql(u8, value orelse "", "hidden")) { self.@"window-show-tab-bar" = .never; return true; }
return false;}
fn compatGtkSingleInstance( self: *Config, alloc: Allocator, key: []const u8, value: ?[]const u8,) bool { _ = alloc; assert(std.mem.eql(u8, key, "gtk-single-instance"));
if (std.mem.eql(u8, value orelse "", "desktop")) { self.@"gtk-single-instance" = .detect; return true; }
return false;}
fn compatCursorInvertFgBg( self: *Config, alloc: Allocator, key: []const u8, value_: ?[]const u8,) bool { _ = alloc; assert(std.mem.eql(u8, key, "cursor-invert-fg-bg"));
// We don't do anything if the value is unset, which is technically // not EXACTLY the same as prior behavior since it would fallback // to doing whatever cursor-color/cursor-text were set to, but // I don't want to store what that is separately so this is close // enough. // // Realistically, these fields were mutually exclusive so anyone // relying on that behavior should just upgrade to the new // cursor-color/cursor-text fields. const set = cli.args.parseBool(value_ orelse "t") catch return false; if (set) { self.@"cursor-color" = .@"cell-foreground"; self.@"cursor-text" = .@"cell-background"; }
return true;}
fn compatSelectionInvertFgBg( self: *Config, alloc: Allocator, key: []const u8, value_: ?[]const u8,) bool { _ = alloc; assert(std.mem.eql(u8, key, "selection-invert-fg-bg"));
const set = cli.args.parseBool(value_ orelse "t") catch return false; if (set) { self.@"selection-foreground" = .@"cell-background"; self.@"selection-background" = .@"cell-foreground"; }
return true;}
fn compatBoldIsBright( self: *Config, alloc: Allocator, key: []const u8, value_: ?[]const u8,) bool { _ = alloc; assert(std.mem.eql(u8, key, "bold-is-bright"));
const isset = cli.args.parseBool(value_ orelse "t") catch return false; if (isset) { self.@"bold-color" = .bright; }
return true;}
fn compatMacOSDockDropBehavior( self: *Config, alloc: Allocator, key: []const u8, value: ?[]const u8,) bool { _ = alloc; assert(std.mem.eql(u8, key, "macos-dock-drop-behavior"));
if (std.mem.eql(u8, value orelse "", "window")) { self.@"macos-dock-drop-behavior" = .@"new-window"; return true; }
return false;}
/// Add a diagnostic message to the config with the given string./// This is always added with a location of "none".pub fn addDiagnosticFmt( self: *Config, comptime fmt: []const u8, args: anytype,) Allocator.Error!void { const alloc = self._arena.?.allocator(); try self._diagnostics.append(alloc, .{ .message = try std.fmt.allocPrintSentinel( alloc, fmt, args, 0, ), });}
/// Create a shallow copy of this config. This will share all the memory/// allocated with the previous config but will have a new arena for/// any changes or new allocations. The config should have `deinit`/// called when it is complete.////// Beware: these shallow clones are not meant for a long lifetime,/// they are just meant to exist temporarily for the duration of some/// modifications. It is very important that the original config not/// be deallocated while shallow clones exist.pub fn shallowClone(self: *const Config, alloc_gpa: Allocator) Config { var result = self.*; result._arena = .init(alloc_gpa); return result;}
/// Create a copy of the metadata of this configuration but without/// the actual values. Metadata includes conditional state.pub fn cloneEmpty( self: *const Config, alloc_gpa: Allocator,) Allocator.Error!Config { var result = try default(alloc_gpa); result._conditional_state = self._conditional_state; return result;}
/// Create a copy of this configuration.////// This will not re-read referenced configuration files and operates/// purely in-memory.pub fn clone( self: *const Config, alloc_gpa: Allocator,) Allocator.Error!Config { // Start with an empty config var result = try self.cloneEmpty(alloc_gpa); errdefer result.deinit(); const alloc_arena = result._arena.?.allocator();
// Copy our values inline for (@typeInfo(Config).@"struct".fields) |field| { if (!@hasField(Key, field.name)) continue; @field(result, field.name) = try cloneValue( alloc_arena, field.type, @field(self, field.name), ); }
// Copy our diagnostics result._diagnostics = try self._diagnostics.clone(alloc_arena);
// Preserve our replay steps. We copy them exactly to also preserve // the exact conditionals required for some steps. try result._replay_steps.ensureTotalCapacity( alloc_arena, self._replay_steps.items.len, ); for (self._replay_steps.items) |item| { result._replay_steps.appendAssumeCapacity( try item.clone(alloc_arena), ); } assert(result._replay_steps.items.len == self._replay_steps.items.len);
// Copy the conditional set result._conditional_set = self._conditional_set;
return result;}
fn cloneValue( alloc: Allocator, comptime T: type, src: T,) Allocator.Error!T { // Do known named types first switch (T) { []const u8 => return try alloc.dupe(u8, src), [:0]const u8 => return try alloc.dupeZ(u8, src),
else => {}, }
// If we're a type that can have decls and we have clone, then // call clone and be done. const t = @typeInfo(T); if (t == .@"struct" or t == .@"enum" or t == .@"union") { if (@hasDecl(T, "clone")) return try src.clone(alloc); }
// Back into types of types switch (t) { inline .bool, .int, .float, .@"enum", .@"union", => return src,
.optional => |info| return try cloneValue( alloc, info.child, src orelse return null, ),
.@"struct" => |info| { // Packed structs we can return directly as copies. assert(info.layout == .@"packed"); return src; },
else => { @compileLog(T); @compileError("unsupported field type"); }, }}
/// Returns an iterator that goes through each changed field from/// old to new. The order of old or new do not matter.pub fn changeIterator(old: *const Config, new: *const Config) ChangeIterator { return .{ .old = old, .new = new, };}
/// Returns true if the given key has changed from old to new. This/// requires the key to be comptime known to make this more efficient.pub fn changed(self: *const Config, new: *const Config, comptime key: Key) bool { // Get the field at comptime const field = comptime field: { const fields = std.meta.fields(Config); for (fields) |field| { if (@field(Key, field.name) == key) { break :field field; } }
unreachable; };
const old_value = @field(self, field.name); const new_value = @field(new, field.name); return !deepEqual(field.type, old_value, new_value);}
/// This yields a key for every changed field between old and new.pub const ChangeIterator = struct { old: *const Config, new: *const Config, i: usize = 0,
pub fn next(self: *ChangeIterator) ?Key { const fields = comptime std.meta.fields(Key); while (self.i < fields.len) { switch (self.i) { inline 0...(fields.len - 1) => |i| { const field = fields[i]; const key = @field(Key, field.name); self.i += 1; if (self.old.changed(self.new, key)) return key; },
else => unreachable, } }
return null; }};
/// This runs a heuristic to determine if we are likely running/// Ghostty in a CLI environment. We need this to change some behaviors./// We should keep the set of behaviors that depend on this as small/// as possible because magic sucks, but each place is well documented.fn probableCliEnvironment() bool { switch (builtin.os.tag) { // Windows has its own problems, just ignore it for now since // its not a real supported target and GTK via WSL2 assuming // single instance is probably fine. .windows => return false,
// On macOS, we don't want to detect `open` calls as CLI envs. // Our desktop detection on macOS is very accurate due to how // processes are launched on macOS, so if we detect we're launched // from the app bundle then we're not in a CLI environment. .macos => if (internal_os.launchedFromDesktop()) return false,
else => {}, }
// If we have TERM_PROGRAM set to a non-empty value, we assume // a graphical terminal environment. if (std.posix.getenv("TERM_PROGRAM")) |v| { if (v.len > 0) return true; }
// CLI arguments makes things probable if (std.os.argv.len > 1) return true;
// Unlikely CLI environment return false;}
/// This is used to "replay" the configuration. See loadTheme for details.const Replay = struct { const Step = union(enum) { /// An argument to parse as if it came from the CLI or file. arg: [:0]const u8,
/// A base path to expand relative paths against. expand: []const u8,
/// A conditional argument. This arg is parsed only if all /// conditions match (an "AND"). An "OR" can be achieved by /// having multiple conditional arg entries. conditional_arg: struct { conditions: []const Conditional, arg: []const u8, },
/// A diagnostic to be added to the new configuration when /// replayed. This should only be used for diagnostics that won't /// be reproduced during playback. For example, `config-file` /// errors are not reloaded so they should be added here. /// /// Diagnostics cannot be conditional. They are always present /// even if the conditionals don't match. This helps users find /// errors in their configuration. diagnostic: cli.Diagnostic,
/// The start of a "-e" argument. This marks the end of /// traditional configuration and the beginning of the /// "-e" initial command magic. This is separate from "arg" /// because there are some behaviors unique to this (i.e. /// we want to keep this at the end for config-file). /// /// Note: when "-e" is used, ONLY this is present and /// not an additional "arg" with "-e" value. @"-e",
fn clone( self: Step, alloc: Allocator, ) Allocator.Error!Step { return switch (self) { .@"-e" => self, .diagnostic => |v| .{ .diagnostic = try v.clone(alloc) }, .arg => |v| .{ .arg = try alloc.dupeZ(u8, v) }, .expand => |v| .{ .expand = try alloc.dupe(u8, v) }, .conditional_arg => |v| conditional: { var conds = try alloc.alloc(Conditional, v.conditions.len); for (v.conditions, 0..) |cond, i| conds[i] = try cond.clone(alloc); break :conditional .{ .conditional_arg = .{ .conditions = conds, .arg = try alloc.dupe(u8, v.arg), } }; }, }; } };
const Iterator = struct { const Self = @This();
config: *Config, slice: []const Replay.Step, idx: usize = 0,
pub fn next(self: *Self) ?[]const u8 { while (true) { if (self.idx >= self.slice.len) return null; defer self.idx += 1; switch (self.slice[self.idx]) { .expand => |base| self.config.expandPaths(base) catch |err| { // This shouldn't happen because to reach this step // means that it succeeded before. Its possible since // expanding paths is a side effect process that the // world state changed and we can't expand anymore. // In that really unfortunate case, we log a warning. log.warn("error expanding paths err={}", .{err}); },
.diagnostic => |diag| diag: { // Best effort to clone and append the diagnostic. // If it fails we log a warning and continue. const arena_alloc = self.config._arena.?.allocator(); const cloned = diag.clone(arena_alloc) catch |err| { log.warn("error cloning diagnostic err={}", .{err}); break :diag; }; self.config._diagnostics.append(arena_alloc, cloned) catch |err| { log.warn("error appending diagnostic err={}", .{err}); break :diag; }; },
.conditional_arg => |v| conditional: { // All conditions must match. for (v.conditions) |cond| { if (!self.config._conditional_state.match(cond)) { break :conditional; } }
return v.arg; },
.arg => |arg| return arg, .@"-e" => return "-e", } } } };
/// Construct a Replay iterator from a slice of replay elements. /// This can be used with args.parse and handles intermediate /// steps such as expanding relative paths. fn iterator(slice: []const Replay.Step, dst: *Config) Iterator { return .{ .slice = slice, .config = dst }; }};
/// Valid values for confirm-close-surface/// c_int because it needs to be extern compatible/// If this is changed, you must also update ghostty.hpub const ConfirmCloseSurface = enum(c_int) { false, true, always,};
/// Valid values for custom-shader-animation/// c_int because it needs to be extern compatible/// If this is changed, you must also update ghostty.hpub const CustomShaderAnimation = enum(c_int) { false, true, always,};
/// Valid values for macos-non-native-fullscreen/// c_int because it needs to be extern compatible/// If this is changed, you must also update ghostty.hpub const NonNativeFullscreen = enum(c_int) { false, true, @"visible-menu", @"padded-notch",};
/// Valid values for fullscreen config option/// c_int because it needs to be extern compatible/// If this is changed, you must also update ghostty.hpub const Fullscreen = enum(c_int) { false, true, @"non-native", @"non-native-visible-menu", @"non-native-padded-notch",};
pub const WindowPaddingColor = enum { background, extend, @"extend-always",};
pub const WindowSubtitle = enum { false, @"working-directory",};
pub const LinkPreviews = enum { false, true, osc8,};
/// See working-directorypub const WorkingDirectory = union(enum) { const Self = @This();
/// Resolve to the current user's home directory during config finalize. home,
/// Inherit the working directory from the launching process. inherit,
/// Use an explicit working directory path. This may be not be /// expanded until finalize is called. path: []const u8,
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { var input = input_ orelse return error.ValueRequired; input = std.mem.trim(u8, input, &std.ascii.whitespace); if (input.len == 0) return error.ValueRequired;
// Match path.zig behavior for quoted values. if (input.len >= 2 and input[0] == '"' and input[input.len - 1] == '"') { input = input[1 .. input.len - 1]; }
if (std.mem.eql(u8, input, "home")) { self.* = .home; return; }
if (std.mem.eql(u8, input, "inherit")) { self.* = .inherit; return; }
self.* = .{ .path = try alloc.dupe(u8, input) }; }
/// Expand tilde paths in .path values. pub fn finalize(self: *Self, alloc: Allocator) Allocator.Error!void { const path = switch (self.*) { .path => |path| path, else => return, };
if (!std.mem.startsWith(u8, path, "~/")) return;
var buf: [std.fs.max_path_bytes]u8 = undefined; const expanded = internal_os.expandHome(path, &buf) catch |err| { log.warn( "error expanding home directory for working-directory path={s}: {}", .{ path, err }, ); return; };
if (std.mem.eql(u8, expanded, path)) return; self.* = .{ .path = try alloc.dupe(u8, expanded) }; }
pub fn value(self: Self) ?[]const u8 { return switch (self) { .path => |path| path, .home, .inherit => null, }; }
pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self { return switch (self) { .path => |path| .{ .path = try alloc.dupe(u8, path) }, else => self, }; }
pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .home, .inherit => try formatter.formatEntry([]const u8, @tagName(self)), .path => |path| try formatter.formatEntry([]const u8, path), } }
test "WorkingDirectory parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var wd: Self = .inherit;
try wd.parseCLI(alloc, "inherit"); try testing.expectEqual(.inherit, wd);
try wd.parseCLI(alloc, "home"); try testing.expectEqual(.home, wd);
try wd.parseCLI(alloc, "~/projects/ghostty"); try testing.expectEqualStrings("~/projects/ghostty", wd.path);
try wd.parseCLI(alloc, "\"/tmp path\""); try testing.expectEqualStrings("/tmp path", wd.path); }
test "WorkingDirectory finalize" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
{ var wd: Self = .{ .path = "~/projects/ghostty" }; try wd.finalize(alloc);
var buf: [std.fs.max_path_bytes]u8 = undefined; const expected = internal_os.expandHome( "~/projects/ghostty", &buf, ) catch "~/projects/ghostty"; try testing.expectEqualStrings(expected, wd.value().?); } }};
/// Color represents a color using RGB.////// This is a packed struct so that the C API to read color values just/// works by setting it to a C integer.pub const Color = struct { r: u8, g: u8, b: u8,
/// ghostty_config_color_s pub const C = extern struct { r: u8, g: u8, b: u8, };
pub fn cval(self: Color) Color.C { return .{ .r = self.r, .g = self.g, .b = self.b }; }
/// Convert this to the terminal RGB struct pub fn toTerminalRGB(self: Color) terminal.color.RGB { return .{ .r = self.r, .g = self.g, .b = self.b }; }
pub fn parseCLI(input_: ?[]const u8) !Color { const input = input_ orelse return error.ValueRequired; // Trim any whitespace before processing const trimmed = std.mem.trim(u8, input, " \t");
if (terminal.x11_color.map.get(trimmed)) |rgb| return .{ .r = rgb.r, .g = rgb.g, .b = rgb.b, };
return fromHex(trimmed); }
/// Deep copy of the struct. Required by Config. pub fn clone(self: Color, _: Allocator) error{}!Color { return self; }
/// Compare if two of our value are requal. Required by Config. pub fn equal(self: Color, other: Color) bool { return std.meta.eql(self, other); }
/// Used by Formatter pub fn formatEntry(self: Color, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; try formatter.formatEntry( []const u8, try self.formatBuf(&buf), ); }
/// Format the color as a string. pub fn formatBuf(self: Color, buf: []u8) Allocator.Error![]const u8 { return std.fmt.bufPrint( buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ self.r, self.g, self.b }, ) catch error.OutOfMemory; }
/// fromHex parses a color from a hex value such as #RRGGBB. The "#" /// is optional. pub fn fromHex(input: []const u8) !Color { // Trim the beginning '#' if it exists const trimmed = if (input.len != 0 and input[0] == '#') input[1..] else input; if (trimmed.len != 6 and trimmed.len != 3) return error.InvalidValue;
// Expand short hex values to full hex values const rgb: []const u8 = if (trimmed.len == 3) &.{ trimmed[0], trimmed[0], trimmed[1], trimmed[1], trimmed[2], trimmed[2], } else trimmed;
// Parse the colors two at a time. var result: Color = undefined; comptime var i: usize = 0; inline while (i < 6) : (i += 2) { const v: u8 = ((try std.fmt.charToDigit(rgb[i], 16)) * 16) + try std.fmt.charToDigit(rgb[i + 1], 16);
@field(result, switch (i) { 0 => "r", 2 => "g", 4 => "b", else => unreachable, }) = v; }
return result; }
test "fromHex" { const testing = std.testing;
try testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.fromHex("#000000")); try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C")); try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C")); try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF")); try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFF")); try testing.expectEqual(Color{ .r = 51, .g = 68, .b = 85 }, try Color.fromHex("#345")); }
test "parseCLI from name" { try std.testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.parseCLI("black")); }
test "formatConfig" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var color: Color = .{ .r = 10, .g = 11, .b = 12 }; try color.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = #0a0b0c\n", buf.written()); }
test "parseCLI with whitespace" { const testing = std.testing; try testing.expectEqual( Color{ .r = 0xAA, .g = 0xBB, .b = 0xCC }, try Color.parseCLI(" #AABBCC "), ); try testing.expectEqual( Color{ .r = 0, .g = 0, .b = 0 }, try Color.parseCLI(" black "), ); }};
/// Represents color values that can also reference special color/// values such as "cell-foreground" or "cell-background".pub const TerminalColor = union(enum) { color: Color, @"cell-foreground", @"cell-background",
pub fn parseCLI(input_: ?[]const u8) !TerminalColor { const input = input_ orelse return error.ValueRequired; if (std.mem.eql(u8, input, "cell-foreground")) return .@"cell-foreground"; if (std.mem.eql(u8, input, "cell-background")) return .@"cell-background"; return .{ .color = try Color.parseCLI(input) }; }
pub fn toTerminalRGB(self: TerminalColor) ?terminal.color.RGB { return switch (self) { .color => |v| v.toTerminalRGB(), .@"cell-foreground", .@"cell-background" => null, }; }
/// Used by Formatter pub fn formatEntry(self: TerminalColor, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .color => try self.color.formatEntry(formatter),
.@"cell-foreground", .@"cell-background", => try formatter.formatEntry([:0]const u8, @tagName(self)), } }
test "parseCLI" { const testing = std.testing;
try testing.expectEqual( TerminalColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, try TerminalColor.parseCLI("#4e2a84"), ); try testing.expectEqual( TerminalColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, try TerminalColor.parseCLI("black"), ); try testing.expectEqual( TerminalColor.@"cell-foreground", try TerminalColor.parseCLI("cell-foreground"), ); try testing.expectEqual( TerminalColor.@"cell-background", try TerminalColor.parseCLI("cell-background"), );
try testing.expectError(error.InvalidValue, TerminalColor.parseCLI("a")); }
test "formatConfig" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var sc: TerminalColor = .@"cell-foreground"; try sc.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.written()); }};
/// Represents color values that can be used for bold. See `bold-color`.pub const BoldColor = union(enum) { color: Color, bright,
/// Convert to the terminal-native BoldColor type. pub fn toTerminal(self: BoldColor) terminal.style.Style.BoldColor { return switch (self) { .color => |col| .{ .color = col.toTerminalRGB() }, .bright => .bright, }; }
pub fn parseCLI(input_: ?[]const u8) !BoldColor { const input = input_ orelse return error.ValueRequired; if (std.mem.eql(u8, input, "bright")) return .bright; return .{ .color = try Color.parseCLI(input) }; }
/// Used by Formatter pub fn formatEntry(self: BoldColor, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .color => try self.color.formatEntry(formatter), .bright => try formatter.formatEntry( [:0]const u8, @tagName(self), ), } }
test "parseCLI" { const testing = std.testing;
try testing.expectEqual( BoldColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, try BoldColor.parseCLI("#4e2a84"), ); try testing.expectEqual( BoldColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, try BoldColor.parseCLI("black"), ); try testing.expectEqual( BoldColor.bright, try BoldColor.parseCLI("bright"), );
try testing.expectError(error.InvalidValue, BoldColor.parseCLI("a")); }
test "formatConfig" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var sc: BoldColor = .bright; try sc.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try testing.expectEqualSlices(u8, "a = bright\n", buf.written()); }};
pub const ColorList = struct { const Self = @This();
colors: std.ArrayListUnmanaged(Color) = .{}, colors_c: std.ArrayListUnmanaged(Color.C) = .{},
/// ghostty_config_color_list_s pub const C = extern struct { colors: [*]Color.C, len: usize, };
pub fn cval(self: *const Self) C { return .{ .colors = self.colors_c.items.ptr, .len = self.colors_c.items.len, }; }
pub fn parseCLI( self: *Self, alloc: Allocator, input_: ?[]const u8, ) !void { const input = input_ orelse return error.ValueRequired; if (input.len == 0) return error.ValueRequired;
// Always reset on parse self.* = .{};
// Split the input by commas and parse each color var it = std.mem.tokenizeScalar(u8, input, ','); var count: usize = 0; while (it.next()) |raw| { count += 1; if (count > 64) return error.InvalidValue;
// Trim whitespace from each color value const trimmed = std.mem.trim(u8, raw, " \t"); const color = try Color.parseCLI(trimmed); try self.colors.append(alloc, color); try self.colors_c.append(alloc, color.cval()); }
// If no colors were parsed, we need to return an error if (self.colors.items.len == 0) return error.InvalidValue;
assert(self.colors.items.len == self.colors_c.items.len); }
pub fn clone( self: *const Self, alloc: Allocator, ) Allocator.Error!Self { return .{ .colors = try self.colors.clone(alloc), .colors_c = try self.colors_c.clone(alloc), }; }
/// Compare if two of our value are requal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.colors.items; const itemsB = other.colors.items; if (itemsA.len != itemsB.len) return false; for (itemsA, itemsB) |a, b| { if (!a.equal(b)) return false; } else return true; }
/// Used by Formatter pub fn formatEntry( self: Self, formatter: anytype, ) !void { // If no items, we want to render an empty field. if (self.colors.items.len == 0) { try formatter.formatEntry(void, {}); return; }
// Build up the value of our config. Our buffer size should be // sized to contain all possible maximum values. var buf: [1024]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); for (self.colors.items, 0..) |color, i| { var color_buf: [128]u8 = undefined; const color_str = try color.formatBuf(&color_buf); if (i != 0) writer.writeByte(',') catch return error.OutOfMemory; writer.writeAll(color_str) catch return error.OutOfMemory; }
try formatter.formatEntry( []const u8, writer.buffered(), ); }
test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var p: Self = .{}; try p.parseCLI(alloc, "black,white"); try testing.expectEqual(2, p.colors.items.len);
// Test whitespace handling try p.parseCLI(alloc, "black, white"); // space after comma try testing.expectEqual(2, p.colors.items.len); try p.parseCLI(alloc, "black , white"); // spaces around comma try testing.expectEqual(2, p.colors.items.len); try p.parseCLI(alloc, " black , white "); // extra spaces at ends try testing.expectEqual(2, p.colors.items.len);
// Error cases try testing.expectError(error.ValueRequired, p.parseCLI(alloc, null)); try testing.expectError(error.InvalidValue, p.parseCLI(alloc, " ")); }
test "format" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var p: Self = .{}; try p.parseCLI(alloc, "black,white"); try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = #000000,#ffffff\n", buf.written()); }
test "clone" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var source: Self = .{}; try source.parseCLI(alloc, "#ff0000,#00ff00,#0000ff");
const cloned = try source.clone(alloc);
try testing.expect(source.equal(cloned)); try testing.expectEqual(source.colors_c.items.len, cloned.colors_c.items.len); for (source.colors_c.items, cloned.colors_c.items) |src_c, clone_c| { try testing.expectEqual(src_c.r, clone_c.r); try testing.expectEqual(src_c.g, clone_c.g); try testing.expectEqual(src_c.b, clone_c.b); } }};
/// Palette is the 256 color palette for 256-color mode.pub const Palette = struct { const Self = @This();
/// The actual value that is updated as we parse. value: terminal.color.Palette = terminal.color.default,
/// Keep track of which indexes were manually set by the user. mask: terminal.color.PaletteMask = .initEmpty(),
/// ghostty_config_palette_s pub const C = extern struct { colors: [256]Color.C, };
pub fn cval(self: Self) Palette.C { var result: Palette.C = undefined; for (self.value, 0..) |color, i| { result.colors[i] = Color.C{ .r = color.r, .g = color.g, .b = color.b, }; }
return result; }
pub fn parseCLI( self: *Self, input: ?[]const u8, ) !void { const value = input orelse return error.ValueRequired; const eqlIdx = std.mem.indexOf(u8, value, "=") orelse return error.InvalidValue;
// Parse the key part (trim whitespace) const key = try std.fmt.parseInt( u8, std.mem.trim(u8, value[0..eqlIdx], " \t"), 0, );
// Parse the color part (Color.parseCLI will handle whitespace) const rgb = try Color.parseCLI(value[eqlIdx + 1 ..]); self.value[key] = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b }; self.mask.set(key); }
/// Deep copy of the struct. Required by Config. pub fn clone(self: Self, _: Allocator) error{}!Self { return self; }
/// Compare if two of our value are requal. Required by Config. pub fn equal(self: Self, other: Self) bool { return std.meta.eql(self, other); }
/// Used by Formatter pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; for (0.., self.value) |k, v| { try formatter.formatEntry( []const u8, std.fmt.bufPrint( &buf, "{d}=#{x:0>2}{x:0>2}{x:0>2}", .{ k, v.r, v.g, v.b }, ) catch return error.OutOfMemory, ); } }
test "parseCLI" { const testing = std.testing;
var p: Self = .{}; try p.parseCLI("0=#AABBCC"); try testing.expect(p.value[0].r == 0xAA); try testing.expect(p.value[0].g == 0xBB); try testing.expect(p.value[0].b == 0xCC); try testing.expect(p.mask.isSet(0)); try testing.expect(!p.mask.isSet(1)); }
test "parseCLI base" { const testing = std.testing;
var p: Self = .{};
try p.parseCLI("0b1=#014589"); try p.parseCLI("0o7=#234567"); try p.parseCLI("0xF=#ABCDEF");
try testing.expect(p.value[0b1].r == 0x01); try testing.expect(p.value[0b1].g == 0x45); try testing.expect(p.value[0b1].b == 0x89);
try testing.expect(p.value[0o7].r == 0x23); try testing.expect(p.value[0o7].g == 0x45); try testing.expect(p.value[0o7].b == 0x67);
try testing.expect(p.value[0xF].r == 0xAB); try testing.expect(p.value[0xF].g == 0xCD); try testing.expect(p.value[0xF].b == 0xEF);
try testing.expect(p.mask.isSet(0b1)); try testing.expect(p.mask.isSet(0o7)); try testing.expect(p.mask.isSet(0xF)); try testing.expect(!p.mask.isSet(0)); try testing.expect(!p.mask.isSet(2)); }
test "parseCLI overflow" { const testing = std.testing;
var p: Self = .{}; try testing.expectError(error.Overflow, p.parseCLI("256=#AABBCC")); // Mask should remain empty since parsing failed. try testing.expectEqual(@as(usize, 0), p.mask.count()); }
test "formatConfig" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var list: Self = .{}; try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = 0=#1d1f21\n", buf.written()[0..14]); }
test "parseCLI with whitespace" { const testing = std.testing;
var p: Self = .{}; try p.parseCLI("0 = #AABBCC"); try p.parseCLI(" 1= #DDEEFF "); try p.parseCLI(" 2 = #123456 ");
try testing.expect(p.value[0].r == 0xAA); try testing.expect(p.value[0].g == 0xBB); try testing.expect(p.value[0].b == 0xCC);
try testing.expect(p.value[1].r == 0xDD); try testing.expect(p.value[1].g == 0xEE); try testing.expect(p.value[1].b == 0xFF);
try testing.expect(p.value[2].r == 0x12); try testing.expect(p.value[2].g == 0x34); try testing.expect(p.value[2].b == 0x56);
try testing.expect(p.mask.isSet(0)); try testing.expect(p.mask.isSet(1)); try testing.expect(p.mask.isSet(2)); try testing.expect(!p.mask.isSet(3)); }};
/// RepeatableString is a string value that can be repeated to accumulate/// a list of strings. This isn't called "StringList" because I find that/// sometimes leads to confusion that it _accepts_ a list such as/// comma-separated values.pub const RepeatableString = struct { const Self = @This();
// Allocator for the list is the arena for the parent config. list: std.ArrayListUnmanaged([:0]const u8) = .{},
// If true, then the next value will clear the list and start over // rather than append. This is a bit of a hack but is here to make // the font-family set of configurations work with CLI parsing. overwrite_next: bool = false,
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { const value = input orelse return error.ValueRequired;
// Empty value resets the list if (value.len == 0) { self.list.clearRetainingCapacity(); return; }
// If we're overwriting then we clear before appending if (self.overwrite_next) { self.list.clearRetainingCapacity(); self.overwrite_next = false; }
const copy = try alloc.dupeZ(u8, value); try self.list.append(alloc, copy); }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { // Copy the list and all the strings in the list. var list = try std.ArrayListUnmanaged([:0]const u8).initCapacity( alloc, self.list.items.len, ); errdefer { for (list.items) |item| alloc.free(item); list.deinit(alloc); } for (self.list.items) |item| { const copy = try alloc.dupeZ(u8, item); list.appendAssumeCapacity(copy); }
return .{ .list = list }; }
/// The number of items in the list pub fn count(self: Self) usize { return self.list.items.len; }
/// Compare if two of our value are requal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.list.items; const itemsB = other.list.items; if (itemsA.len != itemsB.len) return false; for (itemsA, itemsB) |a, b| { if (!std.mem.eql(u8, a, b)) return false; } else return true; }
/// Used by Formatter pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { // If no items, we want to render an empty field. if (self.list.items.len == 0) { try formatter.formatEntry(void, {}); return; }
for (self.list.items) |value| { try formatter.formatEntry([]const u8, value); } }
test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "A"); try list.parseCLI(alloc, "B"); try testing.expectEqual(@as(usize, 2), list.list.items.len);
try list.parseCLI(alloc, ""); try testing.expectEqual(@as(usize, 0), list.list.items.len); }
test "parseCLI overwrite" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "A");
// Set our overwrite flag list.overwrite_next = true;
try list.parseCLI(alloc, "B"); try testing.expectEqual(@as(usize, 1), list.list.items.len); try list.parseCLI(alloc, "C"); try testing.expectEqual(@as(usize, 2), list.list.items.len); }
test "formatConfig empty" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var list: Self = .{}; try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); }
test "formatConfig single item" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "A"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); }
test "formatConfig multiple items" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "A"); try list.parseCLI(alloc, "B"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = A\na = B\n", buf.written()); }};
/// SelectionWordChars stores the parsed codepoints for word boundary/// characters used during text selection. The string is parsed once/// during configuration and stored as u21 codepoints for efficient/// lookup during selection operations.pub const SelectionWordChars = struct { const Self = @This();
/// The parsed codepoints. Always includes null (U+0000) at index 0. codepoints: []const u21 = &terminal.selection_codepoints.default_word_boundaries,
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { const value = input orelse return error.ValueRequired;
// Parse string with Zig escape sequence support into codepoints var list: std.ArrayList(u21) = .empty; defer list.deinit(alloc);
// Always include null as first boundary try list.append(alloc, 0);
var it = string.codepointIterator(value); while (it.next() catch return error.InvalidValue) |codepoint| { try list.append(alloc, codepoint); }
self.codepoints = try list.toOwnedSlice(alloc); }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { const copy = try alloc.dupe(u21, self.codepoints); return .{ .codepoints = copy }; }
/// Compare if two values are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { return std.mem.eql(u21, self.codepoints, other.codepoints); }
/// Used by Formatter pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { // Convert codepoints back to UTF-8 string for display var buf: [4096]u8 = undefined; var pos: usize = 0;
// Skip the null character at index 0 for (self.codepoints[1..]) |codepoint| { var utf8_buf: [4]u8 = undefined; const len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch continue; if (pos + len > buf.len) break; @memcpy(buf[pos..][0..len], utf8_buf[0..len]); pos += len; }
try formatter.formatEntry([]const u8, buf[0..pos]); }
test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var chars: Self = .{}; try chars.parseCLI(alloc, " \t;,");
// Should have null + 4 characters try testing.expectEqual(@as(usize, 5), chars.codepoints.len); try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); try testing.expectEqual(@as(u21, ' '), chars.codepoints[1]); try testing.expectEqual(@as(u21, '\t'), chars.codepoints[2]); try testing.expectEqual(@as(u21, ';'), chars.codepoints[3]); try testing.expectEqual(@as(u21, ','), chars.codepoints[4]); }
test "parseCLI escape sequences" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
// \t escape should be parsed as tab var chars: Self = .{}; try chars.parseCLI(alloc, " \\t;,");
try testing.expectEqual(@as(usize, 5), chars.codepoints.len); try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); try testing.expectEqual(@as(u21, ' '), chars.codepoints[1]); try testing.expectEqual(@as(u21, '\t'), chars.codepoints[2]); try testing.expectEqual(@as(u21, ';'), chars.codepoints[3]); try testing.expectEqual(@as(u21, ','), chars.codepoints[4]); }
test "parseCLI backslash escape" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
// \\ should be parsed as a single backslash var chars: Self = .{}; try chars.parseCLI(alloc, "\\\\;");
try testing.expectEqual(@as(usize, 3), chars.codepoints.len); try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); try testing.expectEqual(@as(u21, '\\'), chars.codepoints[1]); try testing.expectEqual(@as(u21, ';'), chars.codepoints[2]); }
test "parseCLI unicode escape" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
// \u{2502} should be parsed as │ var chars: Self = .{}; try chars.parseCLI(alloc, "\\u{2502};");
try testing.expectEqual(@as(usize, 3), chars.codepoints.len); try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); try testing.expectEqual(@as(u21, '│'), chars.codepoints[1]); try testing.expectEqual(@as(u21, ';'), chars.codepoints[2]); }};
/// FontVariation is a repeatable configuration value that sets a single/// font variation value. Font variations are configurations for what/// are often called "variable fonts." The font files usually end in/// "-VF.ttf."////// The value for this is in the format of `id=value` where `id` is the/// 4-character font variation axis identifier and `value` is the/// floating point value for that axis. For more details on font variations/// see the MDN font-variation-settings documentation since this copies that/// behavior almost exactly:////// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settingspub const RepeatableFontVariation = struct { const Self = @This();
// Allocator for the list is the arena for the parent config. list: std.ArrayListUnmanaged(fontpkg.face.Variation) = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { const input = input_ orelse return error.ValueRequired; const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidValue; const whitespace = " \t"; const key = std.mem.trim(u8, input[0..eql_idx], whitespace); const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace); if (key.len != 4) return error.InvalidValue; try self.list.append(alloc, .{ .id = fontpkg.face.Variation.Id.init(@ptrCast(key.ptr)), .value = std.fmt.parseFloat(f64, value) catch return error.InvalidValue, }); }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { return .{ .list = try self.list.clone(alloc), }; }
/// Compare if two of our value are requal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.list.items; const itemsB = other.list.items; if (itemsA.len != itemsB.len) return false; for (itemsA, itemsB) |a, b| { if (!std.meta.eql(a, b)) return false; } else return true; }
/// Used by Formatter pub fn formatEntry( self: Self, formatter: anytype, ) !void { if (self.list.items.len == 0) { try formatter.formatEntry(void, {}); return; }
var buf: [128]u8 = undefined; for (self.list.items) |value| { const str = std.fmt.bufPrint(&buf, "{s}={d}", .{ value.id.str(), value.value, }) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, str); } }
test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "wght=200"); try list.parseCLI(alloc, "slnt=-15");
try testing.expectEqual(@as(usize, 2), list.list.items.len); try testing.expectEqual(fontpkg.face.Variation{ .id = fontpkg.face.Variation.Id.init("wght"), .value = 200, }, list.list.items[0]); try testing.expectEqual(fontpkg.face.Variation{ .id = fontpkg.face.Variation.Id.init("slnt"), .value = -15, }, list.list.items[1]); }
test "parseCLI with whitespace" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "wght =200"); try list.parseCLI(alloc, "slnt= -15");
try testing.expectEqual(@as(usize, 2), list.list.items.len); try testing.expectEqual(fontpkg.face.Variation{ .id = fontpkg.face.Variation.Id.init("wght"), .value = 200, }, list.list.items[0]); try testing.expectEqual(fontpkg.face.Variation{ .id = fontpkg.face.Variation.Id.init("slnt"), .value = -15, }, list.list.items[1]); }
test "formatConfig single" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "wght = 200"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = wght=200\n", buf.written()); }};
/// Returns true if the given key event would trigger a keybinding/// if it were to be processed. This is useful for determining if/// a key event should be sent to the terminal or not.pub fn keyEventIsBinding( self: *Config, event: inputpkg.KeyEvent,) bool { switch (event.action) { .release => return false, .press, .repeat => {}, }
// If we have a keybinding for this event then we return true. return self.keybind.set.getEvent(event) != null;}
/// Stores a set of keybinds.pub const Keybinds = struct { set: inputpkg.Binding.Set = .{},
/// Defined key tables. The default key table is always the root "set", /// which allows all table names to be available without reservation. tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty,
/// The most recent binding target for `chain=` additions. /// /// This is intentionally tracked at the Keybinds level so that chains can /// apply across table boundaries according to parse order. chain_target: union(enum) { root, table: []const u8, } = .root,
pub fn init(self: *Keybinds, alloc: Allocator) !void { // We don't clear the memory because it's in the arena and unlikely // to be free-able anyways (since arenas can only clear the last // allocated value). This isn't a memory leak because the arena // will be freed when the config is freed. self.set = .{}; self.tables = .empty; self.chain_target = .root;
// keybinds for opening and reloading config try self.set.put( alloc, .{ .key = .{ .unicode = ',' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .reload_config = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = ',' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .open_config = {} }, );
{ try self.set.put( alloc, .{ .key = .{ .physical = .copy } }, .{ .copy_to_clipboard = .mixed }, ); try self.set.put( alloc, .{ .key = .{ .physical = .paste } }, .paste_from_clipboard, );
// On non-MacOS desktop envs (Windows, KDE, Gnome, Xfce), ctrl+insert is an // alt keybinding for Copy and shift+ins is an alt keybinding for Paste // // The order of these blocks is important. The *last* added keybind for a given action is // what will display in the menu. We want the more typical keybinds after this block to be // the standard if (!builtin.target.os.tag.isDarwin()) { try self.set.put( alloc, .{ .key = .{ .physical = .insert }, .mods = .{ .ctrl = true } }, .{ .copy_to_clipboard = .mixed }, ); try self.set.put( alloc, .{ .key = .{ .physical = .insert }, .mods = .{ .shift = true } }, .{ .paste_from_clipboard = {} }, ); }
// On macOS we default to super but Linux ctrl+shift since // ctrl+c is to kill the process. const mods: inputpkg.Mods = if (builtin.target.os.tag.isDarwin()) .{ .super = true } else .{ .ctrl = true, .shift = true };
try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'c' }, .mods = mods }, .{ .copy_to_clipboard = .mixed }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'v' }, .mods = mods }, .paste_from_clipboard, .{ .performable = true }, ); }
// Increase font size mapping for keyboards with dedicated plus keys (like german) // Note: this order matters below because the C API will only return // the last keybinding for a given action. The macOS app uses this to // set the expected keybind for the menu. try self.set.put( alloc, .{ .key = .{ .unicode = '=' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); try self.set.put( alloc, .{ .key = .{ .unicode = '+' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, );
try self.set.put( alloc, .{ .key = .{ .unicode = '-' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .decrease_font_size = 1 }, ); try self.set.put( alloc, .{ .key = .{ .unicode = '0' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .reset_font_size = {} }, );
try self.set.put( alloc, .{ .key = .{ .unicode = 'j' }, .mods = .{ .shift = true, .ctrl = true, .super = true } }, .{ .write_screen_file = .copy }, );
try self.set.put( alloc, .{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .write_screen_file = .paste }, );
try self.set.put( alloc, .{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true, .alt = true }) }, .{ .write_screen_file = .open }, );
// Expand Selection try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .shift = true } }, .{ .adjust_selection = .left }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .shift = true } }, .{ .adjust_selection = .right }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .shift = true } }, .{ .adjust_selection = .up }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .shift = true } }, .{ .adjust_selection = .down }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .page_up }, .mods = .{ .shift = true } }, .{ .adjust_selection = .page_up }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .page_down }, .mods = .{ .shift = true } }, .{ .adjust_selection = .page_down }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .home }, .mods = .{ .shift = true } }, .{ .adjust_selection = .home }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .end }, .mods = .{ .shift = true } }, .{ .adjust_selection = .end }, .{ .performable = true }, );
// Tabs common to all platforms try self.set.put( alloc, .{ .key = .{ .physical = .tab }, .mods = .{ .ctrl = true, .shift = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, .{ .key = .{ .physical = .tab }, .mods = .{ .ctrl = true } }, .{ .next_tab = {} }, );
// Windowing if (comptime !builtin.target.os.tag.isDarwin()) { try self.set.put( alloc, .{ .key = .{ .unicode = 'n' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_window = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_surface = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'q' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .quit = {} }, ); try self.set.put( alloc, .{ .key = .{ .physical = .f4 }, .mods = .{ .alt = true } }, .{ .close_window = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 't' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_tab = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_tab = .this }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .shift = true } }, .{ .previous_tab = {} }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .shift = true } }, .{ .next_tab = {} }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .page_up }, .mods = .{ .ctrl = true } }, .{ .previous_tab = {} }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .page_down }, .mods = .{ .ctrl = true } }, .{ .next_tab = {} }, .{ .performable = true }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'o' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .right }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'e' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .down }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = '[' }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .previous }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = ']' }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .next }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .up }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .down }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .left }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .right }, .{ .performable = true }, );
// Resizing splits try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .up, 10 } }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .down, 10 } }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .left, 10 } }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .right, 10 } }, .{ .performable = true }, );
// Viewport scrolling try self.set.put( alloc, .{ .key = .{ .physical = .home }, .mods = .{ .shift = true } }, .{ .scroll_to_top = {} }, ); try self.set.put( alloc, .{ .key = .{ .physical = .end }, .mods = .{ .shift = true } }, .{ .scroll_to_bottom = {} }, ); try self.set.put( alloc, .{ .key = .{ .physical = .page_up }, .mods = .{ .shift = true } }, .{ .scroll_page_up = {} }, ); try self.set.put( alloc, .{ .key = .{ .physical = .page_down }, .mods = .{ .shift = true } }, .{ .scroll_page_down = {} }, );
// Semantic prompts try self.set.put( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .shift = true, .ctrl = true } }, .{ .jump_to_prompt = -1 }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .shift = true, .ctrl = true } }, .{ .jump_to_prompt = 1 }, );
// Move tab try self.set.putFlags( alloc, .{ .key = .{ .physical = .page_up }, .mods = .{ .shift = true, .ctrl = true } }, .{ .move_tab = -1 }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .page_down }, .mods = .{ .shift = true, .ctrl = true } }, .{ .move_tab = 1 }, .{ .performable = true }, );
// Search try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'f' }, .mods = .{ .ctrl = true, .shift = true } }, .start_search, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .escape } }, .end_search, .{ .performable = true }, );
// Inspector, matching Chromium try self.set.put( alloc, .{ .key = .{ .unicode = 'i' }, .mods = .{ .shift = true, .ctrl = true } }, .{ .inspector = .toggle }, );
// Terminal try self.set.put( alloc, .{ .key = .{ .unicode = 'a' }, .mods = .{ .shift = true, .ctrl = true } }, .{ .select_all = {} }, );
// Selection clipboard paste try self.set.put( alloc, .{ .key = .{ .physical = .insert }, .mods = .{ .shift = true } }, .{ .paste_from_selection = {} }, ); } { // On macOS we default to super but everywhere else // is alt. const mods: inputpkg.Mods = if (builtin.target.os.tag.isDarwin()) .{ .super = true } else .{ .alt = true };
// Cmd/Alt+N for goto tab N const start: u21 = '1'; const end: u21 = '8'; comptime var i: u21 = start; inline while (i <= end) : (i += 1) { // We register BOTH the physical `digit_N` key and the unicode // `N` key. This allows most keyboard layouts to work with // this shortcut. Namely, AZERTY doesn't produce unicode `N` // for their digit keys (they're on shifted keys on the same // physical keys).
try self.set.putFlags( alloc, .{ .key = .{ .physical = @field( inputpkg.Key, std.fmt.comptimePrint("digit_{u}", .{i}), ) }, .mods = mods, }, .{ .goto_tab = (i - start) + 1 }, .{ // On macOS we keep this not performable so that the // keyboard shortcuts in tabs work. In the future the // correct fix is to fix the reverse mapping lookup // to allow us to lookup performable keybinds // conditionally. .performable = !builtin.target.os.tag.isDarwin(), }, );
// Important: this must be the LAST binding set so that the // libghostty trigger API returns this one for the action, // so that things like the macOS tab bar key equivalent label // work properly. try self.set.putFlags( alloc, .{ .key = .{ .unicode = i }, .mods = mods, }, .{ .goto_tab = (i - start) + 1 }, .{ .performable = !builtin.target.os.tag.isDarwin(), }, ); } try self.set.putFlags( alloc, .{ .key = .{ .unicode = '9' }, .mods = mods, }, .{ .last_tab = {} }, .{ // See comment above with the numeric goto_tab .performable = !builtin.target.os.tag.isDarwin(), }, ); }
// Toggle fullscreen try self.set.put( alloc, .{ .key = .{ .physical = .enter }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .toggle_fullscreen = {} }, );
// Toggle zoom a split try self.set.put( alloc, .{ .key = .{ .physical = .enter }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .toggle_split_zoom = {} }, );
// Toggle command palette, matches VSCode try self.set.put( alloc, .{ .key = .{ .unicode = 'p' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .toggle_command_palette, );
// Mac-specific keyboard bindings. if (comptime builtin.target.os.tag.isDarwin()) { try self.set.put( alloc, .{ .key = .{ .unicode = 'q' }, .mods = .{ .super = true } }, .{ .quit = {} }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'k' }, .mods = .{ .super = true } }, .{ .clear_screen = {} }, .{ .performable = true }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'a' }, .mods = .{ .super = true } }, .{ .select_all = {} }, );
// Undo/redo try self.set.putFlags( alloc, .{ .key = .{ .unicode = 't' }, .mods = .{ .super = true, .shift = true } }, .{ .undo = {} }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'z' }, .mods = .{ .super = true } }, .{ .undo = {} }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'z' }, .mods = .{ .super = true, .shift = true } }, .{ .redo = {} }, .{ .performable = true }, );
// Viewport scrolling try self.set.put( alloc, .{ .key = .{ .physical = .home }, .mods = .{ .super = true } }, .{ .scroll_to_top = {} }, ); try self.set.put( alloc, .{ .key = .{ .physical = .end }, .mods = .{ .super = true } }, .{ .scroll_to_bottom = {} }, ); try self.set.put( alloc, .{ .key = .{ .physical = .page_up }, .mods = .{ .super = true } }, .{ .scroll_page_up = {} }, ); try self.set.put( alloc, .{ .key = .{ .physical = .page_down }, .mods = .{ .super = true } }, .{ .scroll_page_down = {} }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'j' }, .mods = .{ .super = true } }, .{ .scroll_to_selection = {} }, .{ .performable = true }, );
// Semantic prompts try self.set.put( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .shift = true } }, .{ .jump_to_prompt = -1 }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .shift = true } }, .{ .jump_to_prompt = 1 }, );
// Mac windowing try self.set.put( alloc, .{ .key = .{ .unicode = 'n' }, .mods = .{ .super = true } }, .{ .new_window = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true } }, .{ .close_surface = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } }, .{ .close_tab = .this }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .shift = true } }, .{ .close_window = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .shift = true, .alt = true } }, .{ .close_all_windows = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 't' }, .mods = .{ .super = true } }, .{ .new_tab = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = '[' }, .mods = .{ .super = true, .shift = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = ']' }, .mods = .{ .super = true, .shift = true } }, .{ .next_tab = {} }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'd' }, .mods = .{ .super = true } }, .{ .new_split = .right }, ); try self.set.put( alloc, .{ .key = .{ .unicode = 'd' }, .mods = .{ .super = true, .shift = true } }, .{ .new_split = .down }, ); try self.set.put( alloc, .{ .key = .{ .unicode = '[' }, .mods = .{ .super = true } }, .{ .goto_split = .previous }, ); try self.set.put( alloc, .{ .key = .{ .unicode = ']' }, .mods = .{ .super = true } }, .{ .goto_split = .next }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .up }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .down }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .left }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .right }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .up, 10 } }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .down, 10 } }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .left, 10 } }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .right, 10 } }, ); try self.set.put( alloc, .{ .key = .{ .unicode = '=' }, .mods = .{ .super = true, .ctrl = true } }, .{ .equalize_splits = {} }, );
// Jump to prompt, matches Terminal.app try self.set.put( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true } }, .{ .jump_to_prompt = -1 }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true } }, .{ .jump_to_prompt = 1 }, );
// Search try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true } }, .start_search, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'e' }, .mods = .{ .super = true } }, .search_selection, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true, .shift = true } }, .end_search, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .physical = .escape } }, .end_search, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'g' }, .mods = .{ .super = true } }, .{ .navigate_search = .next }, .{ .performable = true }, ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'g' }, .mods = .{ .super = true, .shift = true } }, .{ .navigate_search = .previous }, .{ .performable = true }, );
// Inspector, matching Chromium try self.set.put( alloc, .{ .key = .{ .unicode = 'i' }, .mods = .{ .alt = true, .super = true } }, .{ .inspector = .toggle }, );
// Alternate keybind, common to Mac programs try self.set.put( alloc, .{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true, .ctrl = true } }, .{ .toggle_fullscreen = {} }, );
// Selection clipboard paste, matches Terminal.app try self.set.put( alloc, .{ .key = .{ .unicode = 'v' }, .mods = .{ .super = true, .shift = true } }, .{ .paste_from_selection = {} }, );
// "Natural text editing" keybinds. This forces these keys to go back // to legacy encoding (not fixterms). It seems macOS users more than // others are used to these keys so we set them as defaults. If // people want to get back to the fixterm encoding they can set // the keybinds to `unbind`. try self.set.put( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true } }, .{ .text = "\\x05" }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true } }, .{ .text = "\\x01" }, ); try self.set.put( alloc, .{ .key = .{ .physical = .backspace }, .mods = .{ .super = true } }, .{ .text = "\\x15" }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .alt = true } }, .{ .esc = "b" }, ); try self.set.put( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .alt = true } }, .{ .esc = "f" }, ); } }
pub fn parseCLI(self: *Keybinds, alloc: Allocator, input: ?[]const u8) !void { var copy: ?[]u8 = null; const value = value: { const value = input orelse return error.ValueRequired;
// If we don't have a colon, use the value as-is, no copy if (std.mem.indexOf(u8, value, ":") == null) break :value value;
// If we have a colon, we copy the whole value for now. We could // do this more efficiently later if we wanted to. const buf = try alloc.alloc(u8, value.len); copy = buf;
@memcpy(buf, value); break :value buf; }; errdefer if (copy) |v| alloc.free(v);
// Check for special values if (value.len == 0) { log.info("config has 'keybind =', using default keybinds", .{}); try self.init(alloc); return; }
if (std.mem.eql(u8, value, "clear")) { // We don't clear the memory because its in the arena and unlikely // to be free-able anyways (since arenas can only clear the last // allocated value). This isn't a memory leak because the arena // will be freed when the config is freed. log.info("config has 'keybind = clear', all keybinds cleared", .{}); self.set = .{}; self.tables = .empty; self.chain_target = .root; return; }
// Check for table syntax: "name/" or "name/binding" // We look for '/' only before the first '=' to avoid matching // action arguments like "foo=text:/hello". const eq_idx = std.mem.indexOfScalar(u8, value, '=') orelse value.len; if (std.mem.indexOfScalar(u8, value[0..eq_idx], '/')) |slash_idx| table: { const table_name = value[0..slash_idx];
// Length zero is valid, so you can set `/=action` for the slash key if (table_name.len == 0) break :table;
// Ignore '+', '>' because they can be part of sequences and // triggers. This lets things like `ctrl+/=action` work. if (std.mem.indexOfAny( u8, table_name, "+>", ) != null) break :table;
const binding = value[slash_idx + 1 ..];
// Get or create the table const gop = try self.tables.getOrPut(alloc, table_name); if (!gop.found_existing) { // We need to copy our table name into the arena // for valid lookups later. gop.key_ptr.* = try alloc.dupe(u8, table_name); gop.value_ptr.* = .{}; }
// If there's no binding after the slash, this is a table // definition/clear command if (binding.len == 0) { log.debug("config has 'keybind = {s}/', table cleared", .{table_name}); gop.value_ptr.* = .{}; self.chain_target = .root; return; }
// Chains are only allowed at the root level. Their target is // tracked globally by parse order in `self.chain_target`. if (std.mem.startsWith(u8, binding, "chain=")) { return error.InvalidFormat; }
// Parse and add the binding to the table try gop.value_ptr.parseAndPut(alloc, binding); self.chain_target = .{ .table = gop.key_ptr.* }; return; }
if (std.mem.startsWith(u8, value, "chain=")) { switch (self.chain_target) { .root => try self.set.parseAndPut(alloc, value), .table => |table_name| { const table = self.tables.getPtr(table_name) orelse { self.chain_target = .root; return error.InvalidFormat; }; try table.parseAndPut(alloc, value); }, } return; }
// Parse into default set try self.set.parseAndPut(alloc, value); self.chain_target = .root; }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Keybinds, alloc: Allocator) Allocator.Error!Keybinds { var tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty; try tables.ensureTotalCapacity(alloc, @intCast(self.tables.count())); var it = self.tables.iterator(); while (it.next()) |entry| { const key = try alloc.dupe(u8, entry.key_ptr.*); tables.putAssumeCapacity(key, try entry.value_ptr.clone(alloc)); }
return .{ .set = try self.set.clone(alloc), .tables = tables, }; }
/// Compare if two of our value are requal. Required by Config. pub fn equal(self: Keybinds, other: Keybinds) bool { if (!equalSet(&self.set, &other.set)) return false;
// Compare tables if (self.tables.count() != other.tables.count()) return false;
var it = self.tables.iterator(); while (it.next()) |entry| { const other_set = other.tables.get(entry.key_ptr.*) orelse return false; if (!equalSet(entry.value_ptr, &other_set)) return false; }
return true; }
fn equalSet( self: *const inputpkg.Binding.Set, other: *const inputpkg.Binding.Set, ) bool { // Two keybinds are considered equal if their primary bindings // are the same. We don't compare reverse mappings and such. const self_map = &self.bindings; const other_map = &other.bindings;
// If the count of mappings isn't identical they can't be equal if (self_map.count() != other_map.count()) return false;
var it = self_map.iterator(); while (it.next()) |self_entry| { // If the trigger isn't in the other map, they can't be equal const other_entry = other_map.getEntry(self_entry.key_ptr.*) orelse return false;
// If the entry types are different, they can't be equal if (std.meta.activeTag(self_entry.value_ptr.*) != std.meta.activeTag(other_entry.value_ptr.*)) return false;
switch (self_entry.value_ptr.*) { // They're equal if both leader sets are equal. .leader => if (!equalSet( self_entry.value_ptr.*.leader, other_entry.value_ptr.*.leader, )) return false,
// Actions are compared by field directly .leaf => { const self_leaf = self_entry.value_ptr.*.leaf; const other_leaf = other_entry.value_ptr.*.leaf;
if (!deepEqual( inputpkg.Binding.Set.Leaf, self_leaf, other_leaf, )) return false; },
.leaf_chained => { const self_chain = self_entry.value_ptr.*.leaf_chained; const other_chain = other_entry.value_ptr.*.leaf_chained;
if (self_chain.flags != other_chain.flags) return false; if (self_chain.actions.items.len != other_chain.actions.items.len) return false; for (self_chain.actions.items, other_chain.actions.items) |a1, a2| { if (!deepEqual( inputpkg.Binding.Action, a1, a2, )) return false; } }, } }
return true; }
/// Like formatEntry but has an option to include docs. pub fn formatEntryDocs(self: Keybinds, formatter: formatterpkg.EntryFormatter, docs: bool) !void { if (self.set.bindings.count() == 0 and self.tables.count() == 0) { try formatter.formatEntry(void, {}); return; }
var buf: [1024]u8 = undefined;
// Format root set bindings var iter = self.set.bindings.iterator(); while (iter.next()) |next| { const k = next.key_ptr.*; const v = next.value_ptr.*; if (docs) { try formatter.writer.writeAll("\n"); const name = @tagName(v); inline for (@typeInfo(help_strings.KeybindAction).@"struct".decls) |decl| { if (std.mem.eql(u8, decl.name, name)) { const help = @field(help_strings.KeybindAction, decl.name); try formatter.writer.writeAll("# " ++ decl.name ++ "\n"); var lines = std.mem.splitScalar(u8, help, '\n'); while (lines.next()) |line| { try formatter.writer.writeAll("# "); try formatter.writer.writeAll(line); try formatter.writer.writeAll("\n"); } break; } } }
var writer: std.Io.Writer = .fixed(&buf); writer.print("{f}", .{k}) catch return error.OutOfMemory; try v.formatEntries(&writer, formatter); }
// Format table bindings var table_iter = self.tables.iterator(); while (table_iter.next()) |table_entry| { const table_name = table_entry.key_ptr.*; const table_set = table_entry.value_ptr.*;
var binding_iter = table_set.bindings.iterator(); while (binding_iter.next()) |next| { const k = next.key_ptr.*; const v = next.value_ptr.*;
var writer: std.Io.Writer = .fixed(&buf); writer.print("{s}/{f}", .{ table_name, k }) catch return error.OutOfMemory; try v.formatEntries(&writer, formatter); } } }
/// Used by Formatter pub fn formatEntry(self: Keybinds, formatter: formatterpkg.EntryFormatter) !void { try self.formatEntryDocs(formatter, false); }
test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var set: Keybinds = .{}; try set.parseCLI(alloc, "shift+a=copy_to_clipboard"); try set.parseCLI(alloc, "shift+a=csi:hello"); }
test "formatConfig single" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Keybinds = .{}; try list.parseCLI(alloc, "shift+a=csi:hello"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.written()); }
// Regression test for https://github.com/ghostty-org/ghostty/issues/2734 test "formatConfig multiple items" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Keybinds = .{}; try list.parseCLI(alloc, "ctrl+z>1=goto_tab:1"); try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2"); try list.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer));
// Note they turn into translated keys because they match // their ASCII mapping. const want = \\keybind = ctrl+z>1=goto_tab:1 \\keybind = ctrl+z>2=goto_tab:2 \\ ; try std.testing.expectEqualStrings(want, buf.written()); }
test "formatConfig multiple items nested" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Keybinds = .{}; try list.parseCLI(alloc, "ctrl+a>ctrl+b>n=new_window"); try list.parseCLI(alloc, "ctrl+a>ctrl+b>w=close_window"); try list.parseCLI(alloc, "ctrl+a>ctrl+c>t=new_tab"); try list.parseCLI(alloc, "ctrl+b>ctrl+d>a=previous_tab"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer));
// NB: This does not currently retain the order of the keybinds. const want = \\a = ctrl+a>ctrl+b>n=new_window \\a = ctrl+a>ctrl+b>w=close_window \\a = ctrl+a>ctrl+c>t=new_tab \\a = ctrl+b>ctrl+d>a=previous_tab \\ ; try std.testing.expectEqualStrings(want, buf.written()); }
test "parseCLI table definition" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Define a table by adding a binding to it try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); try testing.expectEqual(1, keybinds.tables.count()); try testing.expect(keybinds.tables.contains("foo"));
const table = keybinds.tables.get("foo").?; try testing.expectEqual(1, table.bindings.count()); }
test "parseCLI table clear" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Add a binding to a table try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); try testing.expectEqual(1, keybinds.tables.get("foo").?.bindings.count());
// Clear the table with "foo/" try keybinds.parseCLI(alloc, "foo/"); try testing.expectEqual(0, keybinds.tables.get("foo").?.bindings.count()); }
test "parseCLI table multiple bindings" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window");
try testing.expectEqual(2, keybinds.tables.count()); try testing.expectEqual(2, keybinds.tables.get("foo").?.bindings.count()); try testing.expectEqual(1, keybinds.tables.get("bar").?.bindings.count()); }
test "parseCLI table does not affect root set" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard");
// Root set should have the first binding try testing.expectEqual(1, keybinds.set.bindings.count()); // Table should have the second binding try testing.expectEqual(1, keybinds.tables.get("foo").?.bindings.count()); }
test "parseCLI table empty name is invalid" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{}; try testing.expectError(error.InvalidFormat, keybinds.parseCLI(alloc, "/shift+a=copy_to_clipboard")); }
test "parseCLI table with key sequence" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Key sequences should work within tables try keybinds.parseCLI(alloc, "foo/ctrl+a>ctrl+b=new_window");
const table = keybinds.tables.get("foo").?; try testing.expectEqual(1, table.bindings.count()); }
test "parseCLI slash in action argument is not a table" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// A slash after the = should not be interpreted as a table delimiter try keybinds.parseCLI(alloc, "ctrl+a=text:/hello");
// Should be in root set, not a table try testing.expectEqual(1, keybinds.set.bindings.count()); try testing.expectEqual(0, keybinds.tables.count()); }
test "parseCLI slash as key with modifier is not a table" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// ctrl+/ should be parsed as a keybind with '/' as the key, not a table try keybinds.parseCLI(alloc, "ctrl+/=text:foo");
// Should be in root set, not a table try testing.expectEqual(1, keybinds.set.bindings.count()); try testing.expectEqual(0, keybinds.tables.count()); }
test "parseCLI shift+slash as key is not a table" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// shift+/ should be parsed as a keybind, not a table try keybinds.parseCLI(alloc, "shift+/=ignore");
// Should be in root set, not a table try testing.expectEqual(1, keybinds.set.bindings.count()); try testing.expectEqual(0, keybinds.tables.count()); }
test "parseCLI bare slash as key is not a table" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Bare / as a key should work (empty table name is rejected) try keybinds.parseCLI(alloc, "/=text:foo");
// Should be in root set, not a table try testing.expectEqual(1, keybinds.set.bindings.count()); try testing.expectEqual(0, keybinds.tables.count()); }
test "parseCLI slash in key sequence is not a table" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Key sequence ending with / should work try keybinds.parseCLI(alloc, "ctrl+a>ctrl+/=new_window");
// Should be in root set, not a table try testing.expectEqual(1, keybinds.set.bindings.count()); try testing.expectEqual(0, keybinds.tables.count()); }
test "parseCLI table with slash in binding" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Table with a binding that uses / as the key try keybinds.parseCLI(alloc, "mytable//=text:foo");
// Should be in the table try testing.expectEqual(0, keybinds.set.bindings.count()); try testing.expectEqual(1, keybinds.tables.count()); try testing.expect(keybinds.tables.contains("mytable")); try testing.expectEqual(1, keybinds.tables.get("mytable").?.bindings.count()); }
test "parseCLI table with sequence containing slash" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Table with a key sequence that ends with / try keybinds.parseCLI(alloc, "mytable/a>/=new_window");
// Should be in the table try testing.expectEqual(0, keybinds.set.bindings.count()); try testing.expectEqual(1, keybinds.tables.count()); try testing.expect(keybinds.tables.contains("mytable")); }
test "parseCLI chain without prior parsed binding is invalid" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try testing.expectError( error.InvalidFormat, keybinds.parseCLI(alloc, "chain=new_tab"), ); }
test "parseCLI table chain syntax is invalid" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try keybinds.parseCLI(alloc, "foo/a=text:hello"); try testing.expectError( error.InvalidFormat, keybinds.parseCLI(alloc, "foo/chain=deactivate_key_table"), ); }
test "parseCLI chain applies to most recent table binding" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
try keybinds.parseCLI(alloc, "ctrl+n=activate_key_table:foo"); try keybinds.parseCLI(alloc, "foo/a=text:hello"); try keybinds.parseCLI(alloc, "chain=deactivate_key_table");
const root_entry = keybinds.set.get(.{ .mods = .{ .ctrl = true }, .key = .{ .unicode = 'n' }, }).?.value_ptr.*; try testing.expect(root_entry == .leaf); try testing.expect(root_entry.leaf.action == .activate_key_table);
const foo_entry = keybinds.tables.get("foo").?.get(.{ .key = .{ .unicode = 'a' }, }).?.value_ptr.*; try testing.expect(foo_entry == .leaf_chained); try testing.expectEqual(@as(usize, 2), foo_entry.leaf_chained.actions.items.len); try testing.expect(foo_entry.leaf_chained.actions.items[0] == .text); try testing.expect(foo_entry.leaf_chained.actions.items[1] == .deactivate_key_table); }
test "clone with tables" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{}; try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window");
const cloned = try keybinds.clone(alloc);
// Verify the clone has the same structure try testing.expectEqual(keybinds.set.bindings.count(), cloned.set.bindings.count()); try testing.expectEqual(keybinds.tables.count(), cloned.tables.count()); try testing.expectEqual( keybinds.tables.get("foo").?.bindings.count(), cloned.tables.get("foo").?.bindings.count(), ); try testing.expectEqual( keybinds.tables.get("bar").?.bindings.count(), cloned.tables.get("bar").?.bindings.count(), ); }
test "equal with tables" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds1: Keybinds = .{}; try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
var keybinds2: Keybinds = .{}; try keybinds2.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
try testing.expect(keybinds1.equal(keybinds2)); }
test "equal with tables different table count" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds1: Keybinds = .{}; try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
var keybinds2: Keybinds = .{}; try keybinds2.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); try keybinds2.parseCLI(alloc, "bar/shift+b=paste_from_clipboard");
try testing.expect(!keybinds1.equal(keybinds2)); }
test "equal with tables different table names" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds1: Keybinds = .{}; try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
var keybinds2: Keybinds = .{}; try keybinds2.parseCLI(alloc, "bar/shift+a=copy_to_clipboard");
try testing.expect(!keybinds1.equal(keybinds2)); }
test "equal with tables different bindings" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds1: Keybinds = .{}; try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard");
var keybinds2: Keybinds = .{}; try keybinds2.parseCLI(alloc, "foo/shift+b=paste_from_clipboard");
try testing.expect(!keybinds1.equal(keybinds2)); }
test "formatEntry with tables" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{}; try keybinds.parseCLI(alloc, "foo/shift+a=csi:hello"); try keybinds.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer));
try testing.expectEqualStrings("keybind = foo/shift+a=csi:hello\n", buf.written()); }
test "formatEntry with tables and root set" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{}; try keybinds.parseCLI(alloc, "shift+b=csi:world"); try keybinds.parseCLI(alloc, "foo/shift+a=csi:hello"); try keybinds.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer));
const output = buf.written(); try testing.expect(std.mem.indexOf(u8, output, "keybind = shift+b=csi:world\n") != null); try testing.expect(std.mem.indexOf(u8, output, "keybind = foo/shift+a=csi:hello\n") != null); }
test "parseCLI clear clears tables" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Add bindings to root set and tables try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window");
try testing.expectEqual(1, keybinds.set.bindings.count()); try testing.expectEqual(2, keybinds.tables.count());
// Clear all keybinds try keybinds.parseCLI(alloc, "clear");
// Both root set and tables should be cleared try testing.expectEqual(0, keybinds.set.bindings.count()); try testing.expectEqual(0, keybinds.tables.count()); }
test "parseCLI reset clears tables" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var keybinds: Keybinds = .{};
// Add bindings to tables try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); try keybinds.parseCLI(alloc, "bar/shift+b=paste_from_clipboard");
try testing.expectEqual(2, keybinds.tables.count());
// Reset to defaults (empty value) try keybinds.parseCLI(alloc, "");
// Tables should be cleared, root set has defaults try testing.expectEqual(0, keybinds.tables.count()); try testing.expect(keybinds.set.bindings.count() > 0); }};
/// See "font-codepoint-map" for documentation.pub const RepeatableCodepointMap = struct { const Self = @This();
map: fontpkg.CodepointMap = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { const input = input_ orelse return error.ValueRequired; const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidValue; const whitespace = " \t"; const key = std.mem.trim(u8, input[0..eql_idx], whitespace); const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace); const valueZ = try alloc.dupeZ(u8, value);
var p: UnicodeRangeParser = .{ .input = key }; while (try p.next()) |range| { try self.map.add(alloc, .{ .range = range, .descriptor = .{ .family = valueZ, .monospace = false, // we allow any font }, }); } }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { return .{ .map = try self.map.clone(alloc) }; }
/// Compare if two of our value are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.map.list.slice(); const itemsB = other.map.list.slice(); if (itemsA.len != itemsB.len) return false; for (0..itemsA.len) |i| { const a = itemsA.get(i); const b = itemsB.get(i); if (!std.meta.eql(a, b)) return false; } else return true; }
/// Used by Formatter pub fn formatEntry( self: Self, formatter: anytype, ) !void { if (self.map.list.len == 0) { try formatter.formatEntry(void, {}); return; }
var buf: [1024]u8 = undefined; const ranges = self.map.list.items(.range); const descriptors = self.map.list.items(.descriptor); for (ranges, descriptors) |range, descriptor| { if (range[0] == range[1]) { try formatter.formatEntry( []const u8, std.fmt.bufPrint( &buf, "U+{X:0>4}={s}", .{ range[0], descriptor.family orelse "", }, ) catch return error.OutOfMemory, ); } else { try formatter.formatEntry( []const u8, std.fmt.bufPrint( &buf, "U+{X:0>4}-U+{X:0>4}={s}", .{ range[0], range[1], descriptor.family orelse "", }, ) catch return error.OutOfMemory, ); } } }
/// Parses the list of Unicode codepoint ranges. Valid syntax: /// /// "" (empty returns null) /// U+1234 /// U+1234-5678 /// U+1234,U+5678 /// U+1234-5678,U+5678 /// U+1234,U+5678-U+9ABC /// /// etc. const UnicodeRangeParser = struct { input: []const u8, i: usize = 0,
pub fn next(self: *UnicodeRangeParser) !?[2]u21 { // Once we're EOF then we're done without an error. if (self.eof()) return null;
// One codepoint no matter what const start = try self.parseCodepoint(); if (self.eof()) return .{ start, start };
// We're allowed to have any whitespace here self.consumeWhitespace();
// Otherwise we expect either a range or a comma switch (self.input[self.i]) { // Comma means we have another codepoint but in a different // range so we return our current codepoint. ',' => { self.advance(); self.consumeWhitespace(); if (self.eof()) return error.InvalidValue; return .{ start, start }; },
// Hyphen means we have a range. '-' => { self.advance(); self.consumeWhitespace(); if (self.eof()) return error.InvalidValue; const end = try self.parseCodepoint(); self.consumeWhitespace(); if (!self.eof() and self.input[self.i] != ',') return error.InvalidValue; self.advance(); self.consumeWhitespace(); if (start > end) return error.InvalidValue; return .{ start, end }; },
else => return error.InvalidValue, } }
fn consumeWhitespace(self: *UnicodeRangeParser) void { while (!self.eof()) { switch (self.input[self.i]) { ' ', '\t' => self.advance(), else => return, } } }
fn parseCodepoint(self: *UnicodeRangeParser) !u21 { if (self.input[self.i] != 'U') return error.InvalidValue; self.advance(); if (self.eof()) return error.InvalidValue; if (self.input[self.i] != '+') return error.InvalidValue; self.advance(); if (self.eof()) return error.InvalidValue;
const start_i = self.i; while (true) { const current = self.input[self.i]; const is_hex = (current >= '0' and current <= '9') or (current >= 'A' and current <= 'F') or (current >= 'a' and current <= 'f'); if (!is_hex) break;
// Advance but break on EOF self.advance(); if (self.eof()) break; }
// If we didn't consume a single character, we have an error. if (start_i == self.i) return error.InvalidValue;
return std.fmt.parseInt(u21, self.input[start_i..self.i], 16) catch return error.InvalidValue; }
fn advance(self: *UnicodeRangeParser) void { self.i += 1; }
fn eof(self: *const UnicodeRangeParser) bool { return self.i >= self.input.len; } };
test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+ABCD=Comic Sans"); try list.parseCLI(alloc, "U+0001 - U+0005=Verdana"); try list.parseCLI(alloc, "U+0006-U+0009, U+ABCD=Courier");
try testing.expectEqual(@as(usize, 4), list.map.list.len); { const entry = list.map.list.get(0); try testing.expectEqual([2]u21{ 0xABCD, 0xABCD }, entry.range); try testing.expectEqualStrings("Comic Sans", entry.descriptor.family.?); } { const entry = list.map.list.get(1); try testing.expectEqual([2]u21{ 1, 5 }, entry.range); try testing.expectEqualStrings("Verdana", entry.descriptor.family.?); } { const entry = list.map.list.get(2); try testing.expectEqual([2]u21{ 6, 9 }, entry.range); try testing.expectEqualStrings("Courier", entry.descriptor.family.?); } { const entry = list.map.list.get(3); try testing.expectEqual([2]u21{ 0xABCD, 0xABCD }, entry.range); try testing.expectEqualStrings("Courier", entry.descriptor.family.?); } }
test "formatConfig single" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+ABCD=Comic Sans"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = U+ABCD=Comic Sans\n", buf.written()); }
test "formatConfig range" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+0001 - U+0005=Verdana"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = U+0001-U+0005=Verdana\n", buf.written()); }
test "formatConfig multiple" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+0006-U+0009, U+ABCD=Courier"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, \\a = U+0006-U+0009=Courier \\a = U+ABCD=Courier \\ , buf.written()); }};
/// See "clipboard-codepoint-map" for documentation.pub const RepeatableClipboardCodepointMap = struct { const Self = @This();
map: ClipboardCodepointMap = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { const input = input_ orelse return error.ValueRequired; const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidValue; const whitespace = " \t"; const key = std.mem.trim(u8, input[0..eql_idx], whitespace); const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace);
// Parse the replacement value - either a codepoint or string const replacement: ClipboardCodepointMap.Replacement = if (std.mem.startsWith(u8, value, "U+")) blk: { // Parse as codepoint const cp_str = value[2..]; // Skip "U+" const cp = std.fmt.parseInt(u21, cp_str, 16) catch return error.InvalidValue; break :blk .{ .codepoint = cp }; } else blk: { // Parse as UTF-8 string - validate it's valid UTF-8 if (!std.unicode.utf8ValidateSlice(value)) return error.InvalidValue; const value_copy = try alloc.dupe(u8, value); break :blk .{ .string = value_copy }; };
var p: UnicodeRangeParser = .{ .input = key }; while (try p.next()) |range| { try self.map.add(alloc, .{ .range = range, .replacement = replacement, }); } }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { return .{ .map = try self.map.clone(alloc) }; }
/// Compare if two of our value are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.map.list.slice(); const itemsB = other.map.list.slice(); if (itemsA.len != itemsB.len) return false; for (0..itemsA.len) |i| { const a = itemsA.get(i); const b = itemsB.get(i); if (!std.meta.eql(a.range, b.range)) return false; switch (a.replacement) { .codepoint => |cp_a| switch (b.replacement) { .codepoint => |cp_b| if (cp_a != cp_b) return false, .string => return false, }, .string => |str_a| switch (b.replacement) { .string => |str_b| if (!std.mem.eql(u8, str_a, str_b)) return false, .codepoint => return false, }, } } return true; }
/// Used by Formatter pub fn formatEntry( self: Self, formatter: anytype, ) !void { if (self.map.list.len == 0) { try formatter.formatEntry(void, {}); return; }
var buf: [1024]u8 = undefined; var value_buf: [32]u8 = undefined; const ranges = self.map.list.items(.range); const replacements = self.map.list.items(.replacement); for (ranges, replacements) |range, replacement| { const value_str = switch (replacement) { .codepoint => |cp| try std.fmt.bufPrint(&value_buf, "U+{X:0>4}", .{cp}), .string => |s| s, };
if (range[0] == range[1]) { try formatter.formatEntry( []const u8, std.fmt.bufPrint( &buf, "U+{X:0>4}={s}", .{ range[0], value_str }, ) catch return error.OutOfMemory, ); } else { try formatter.formatEntry( []const u8, std.fmt.bufPrint( &buf, "U+{X:0>4}-U+{X:0>4}={s}", .{ range[0], range[1], value_str }, ) catch return error.OutOfMemory, ); } } }
/// Reuse the same UnicodeRangeParser from RepeatableCodepointMap const UnicodeRangeParser = RepeatableCodepointMap.UnicodeRangeParser;
test "parseCLI codepoint replacement" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+2500=U+002D"); // box drawing → hyphen
try testing.expectEqual(@as(usize, 1), list.map.list.len); const entry = list.map.list.get(0); try testing.expectEqual([2]u21{ 0x2500, 0x2500 }, entry.range); try testing.expect(entry.replacement == .codepoint); try testing.expectEqual(@as(u21, 0x002D), entry.replacement.codepoint); }
test "parseCLI string replacement" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+03A3=SUM"); // Greek sigma → "SUM"
try testing.expectEqual(@as(usize, 1), list.map.list.len); const entry = list.map.list.get(0); try testing.expectEqual([2]u21{ 0x03A3, 0x03A3 }, entry.range); try testing.expect(entry.replacement == .string); try testing.expectEqualStrings("SUM", entry.replacement.string); }
test "parseCLI range replacement" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+2500-U+2503=|"); // box drawing range → pipe
try testing.expectEqual(@as(usize, 1), list.map.list.len); const entry = list.map.list.get(0); try testing.expectEqual([2]u21{ 0x2500, 0x2503 }, entry.range); try testing.expect(entry.replacement == .string); try testing.expectEqualStrings("|", entry.replacement.string); }
test "formatConfig codepoint" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+2500=U+002D"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = U+2500=U+002D\n", buf.written()); }
test "formatConfig string" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: Self = .{}; try list.parseCLI(alloc, "U+03A3=SUM"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = U+03A3=SUM\n", buf.written()); }};
pub const FontStyle = union(enum) { const Self = @This();
/// Use the default font style that font discovery finds. default: void,
/// Disable this font style completely. This will fall back to using /// the regular font when this style is encountered. false: void,
/// A specific named font style to use for this style. name: [:0]const u8,
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { const value = input orelse return error.ValueRequired;
if (std.mem.eql(u8, value, "default")) { self.* = .{ .default = {} }; return; }
if (std.mem.eql(u8, value, "false")) { self.* = .{ .false = {} }; return; }
const nameZ = try alloc.dupeZ(u8, value); self.* = .{ .name = nameZ }; }
/// Returns the string name value that can be used with a font /// descriptor. pub fn nameValue(self: Self) ?[:0]const u8 { return switch (self) { .default, .false => null, .name => self.name, }; }
/// Deep copy of the struct. Required by Config. pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self { return switch (self) { .default, .false => self, .name => |v| .{ .name = try alloc.dupeZ(u8, v) }, }; }
/// Used by Formatter pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .default, .false => try formatter.formatEntry( []const u8, @tagName(self), ),
.name => |name| { try formatter.formatEntry([:0]const u8, name); }, } }
test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "default"); try testing.expectEqual(Self{ .default = {} }, p);
try p.parseCLI(alloc, "false"); try testing.expectEqual(Self{ .false = {} }, p);
try p.parseCLI(alloc, "bold"); try testing.expectEqualStrings("bold", p.name); }
test "formatConfig default" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "default"); try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = default\n", buf.written()); }
test "formatConfig false" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "false"); try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = false\n", buf.written()); }
test "formatConfig named" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "bold"); try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = bold\n", buf.written()); }};
/// See `font-synthetic-style` for documentation.pub const FontSyntheticStyle = packed struct { bold: bool = true, italic: bool = true, @"bold-italic": bool = true,};
/// See "font-shaping-break" for documentationpub const FontShapingBreak = packed struct { cursor: bool = true,};
/// See "link" for documentation.pub const RepeatableLink = struct { const Self = @This();
links: std.ArrayListUnmanaged(inputpkg.Link) = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { _ = self; _ = alloc; _ = input_; return error.NotImplemented; }
/// Deep copy of the struct. Required by Config. pub fn clone( self: *const Self, alloc: Allocator, ) Allocator.Error!Self { // Note: we don't do any errdefers below since the allocation // is expected to be arena allocated.
var list = try std.ArrayListUnmanaged(inputpkg.Link).initCapacity( alloc, self.links.items.len, ); for (self.links.items) |item| { const copy = try item.clone(alloc); list.appendAssumeCapacity(copy); }
return .{ .links = list }; }
/// Compare if two of our value are requal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.links.items; const itemsB = other.links.items; if (itemsA.len != itemsB.len) return false; for (itemsA, itemsB) |*a, *b| { if (!a.equal(b)) return false; } else return true; }
/// Used by Formatter pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { // This currently can't be set so we don't format anything. _ = self; _ = formatter; }};
/// Options for copy on select behavior.pub const CopyOnSelect = enum { /// Disables copy on select entirely. false,
/// Copy on select is enabled, but goes to the selection clipboard. /// This is not supported on platforms such as macOS. This is the default. true,
/// Copy on select is enabled and goes to both the system clipboard /// and the selection clipboard (for Linux). clipboard,};
/// Options for right-click actions.pub const RightClickAction = enum { /// No action is taken on right-click. ignore,
/// Pastes from the system clipboard. paste,
/// Copies the selected text to the system clipboard. copy,
/// Copies the selected text to the system clipboard and /// pastes the clipboard if no text is selected. @"copy-or-paste",
/// Shows a context menu with options. @"context-menu",};
/// Options for middle-click actions.pub const MiddleClickAction = enum { /// Paste from the selection/standard clipboard per `copy-on-select`. @"primary-paste",
/// No action is taken on middle click. ignore,};
/// Shell integration valuespub const ShellIntegration = enum { none, detect, bash, elvish, fish, nushell, zsh,};
/// Shell integration featurespub const ShellIntegrationFeatures = packed struct { cursor: bool = true, sudo: bool = false, title: bool = true, @"ssh-env": bool = false, @"ssh-terminfo": bool = false, path: bool = true,};
pub const SplitPreserveZoom = packed struct { navigation: bool = false,};
pub const RepeatableCommand = struct { const Self = @This();
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty, value_c: std.ArrayListUnmanaged(inputpkg.Command.C) = .empty,
/// ghostty_config_command_list_s pub const C = extern struct { commands: [*]inputpkg.Command.C, len: usize, };
pub fn cval(self: *const Self) C { return .{ .commands = self.value_c.items.ptr, .len = self.value_c.items.len, }; }
pub fn init(self: *Self, alloc: Allocator) !void { self.value = .empty; self.value_c = .empty; errdefer { self.value.deinit(alloc); self.value_c.deinit(alloc); } try self.value.appendSlice(alloc, inputpkg.command.defaults); try self.value_c.appendSlice(alloc, inputpkg.command.defaultsC); }
pub fn parseCLI( self: *Self, alloc: Allocator, input_: ?[]const u8, ) !void { // Unset or empty input clears the list const input = input_ orelse ""; if (input.len == 0) { log.info("config has 'command-palette-entry =', using default entries", .{}); try self.init(alloc); return; }
if (std.mem.eql(u8, input, "clear")) { log.info("config has 'command-palette-entry = clear', all command entries cleared", .{}); self.value.clearRetainingCapacity(); self.value_c.clearRetainingCapacity(); return; }
// Reserve space in our lists try self.value.ensureUnusedCapacity(alloc, 1); try self.value_c.ensureUnusedCapacity(alloc, 1);
const cmd = try cli.args.parseAutoStruct( inputpkg.Command, alloc, input, null, ); const cmd_c = try cmd.cval(alloc); self.value.appendAssumeCapacity(cmd); self.value_c.appendAssumeCapacity(cmd_c); }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { const value = try self.value.clone(alloc); for (value.items) |*item| { item.* = try item.clone(alloc); }
return .{ .value = value, .value_c = try self.value_c.clone(alloc), }; }
/// Compare if two of our value are equal. Required by Config. pub fn equal(self: RepeatableCommand, other: RepeatableCommand) bool { if (self.value.items.len != other.value.items.len) return false; for (self.value.items, other.value.items) |a, b| { if (!a.equal(b)) return false; }
return true; }
/// Used by Formatter pub fn formatEntry( self: RepeatableCommand, formatter: formatterpkg.EntryFormatter, ) !void { if (self.value.items.len == 0) { try formatter.formatEntry(void, {}); return; }
for (self.value.items) |item| { var buf: [4096]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf);
writer.print( "title:\"{f}\"", .{std.zig.fmtString(item.title)}, ) catch return error.OutOfMemory;
if (item.description.len > 0) { writer.print( ",description:\"{f}\"", .{std.zig.fmtString(item.description)}, ) catch return error.OutOfMemory; }
writer.print(",action:\"{f}\"", .{item.action}) catch return error.OutOfMemory;
try formatter.formatEntry([]const u8, writer.buffered()); } }
test "RepeatableCommand parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Foo,action:ignore"); try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle"); try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5"); try list.parseCLI(alloc, "title:Baz,description:Raspberry Pie,action:set_font_size:3.14");
try testing.expectEqual(@as(usize, 4), list.value.items.len);
try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action); try testing.expectEqualStrings("Foo", list.value.items[0].title);
try testing.expect(list.value.items[1].action == .text); try testing.expectEqualStrings("ale bydle", list.value.items[1].action.text); try testing.expectEqualStrings("Bar", list.value.items[1].title); try testing.expectEqualStrings("bobr", list.value.items[1].description);
try testing.expectEqual( inputpkg.Binding.Action{ .increase_font_size = 2.5 }, list.value.items[2].action, ); try testing.expectEqualStrings("Quux", list.value.items[2].title); try testing.expectEqualStrings("boo", list.value.items[2].description);
try testing.expectEqual( inputpkg.Binding.Action{ .set_font_size = 3.14 }, list.value.items[3].action, ); try testing.expectEqualStrings("Baz", list.value.items[3].title); try testing.expectEqualStrings("Raspberry Pie", list.value.items[3].description);
try list.parseCLI(alloc, "clear"); try testing.expectEqual(@as(usize, 0), list.value.items.len);
try list.parseCLI(alloc, ""); try testing.expectEqual(inputpkg.command.defaults.len, list.value.items.len); }
test "RepeatableCommand formatConfig empty" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var list: RepeatableCommand = .{}; try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); }
test "RepeatableCommand formatConfig single item" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Bobr, action:text:Bober"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:Bober\"\n", buf.written()); }
test "RepeatableCommand formatConfig multiple items" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Bobr, action:text:kurwa"); try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle"); try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:kurwa\"\na = title:\"Ja\",description:\"pierdole\",action:\"text:jakie bydle\"\n", buf.written()); }
test "RepeatableCommand parseCLI commas" { const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
{ var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:\"Bo,br\",action:\"text:kur,wa\""); try testing.expectEqual(@as(usize, 1), list.value.items.len);
const item = list.value.items[0]; try testing.expectEqualStrings("Bo,br", item.title); try testing.expectEqualStrings("", item.description); try testing.expect(item.action == .text); try testing.expectEqualStrings("kur,wa", item.action.text); } { var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:\"Bo,br\",description:\"abc,def\",action:text:kurwa"); try testing.expectEqual(@as(usize, 1), list.value.items.len);
const item = list.value.items[0]; try testing.expectEqualStrings("Bo,br", item.title); try testing.expectEqualStrings("abc,def", item.description); try testing.expect(item.action == .text); try testing.expectEqualStrings("kurwa", item.action.text); } }
test "RepeatableCommand cval" { const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Foo,action:ignore"); try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle");
try testing.expectEqual(@as(usize, 2), list.value.items.len); try testing.expectEqual(@as(usize, 2), list.value_c.items.len);
const cv = list.cval(); try testing.expectEqual(@as(usize, 2), cv.len);
// First entry try testing.expectEqualStrings("Foo", std.mem.sliceTo(cv.commands[0].title, 0)); try testing.expectEqualStrings("ignore", std.mem.sliceTo(cv.commands[0].action_key, 0)); try testing.expectEqualStrings("ignore", std.mem.sliceTo(cv.commands[0].action, 0));
// Second entry try testing.expectEqualStrings("Bar", std.mem.sliceTo(cv.commands[1].title, 0)); try testing.expectEqualStrings("bobr", std.mem.sliceTo(cv.commands[1].description, 0)); try testing.expectEqualStrings("text", std.mem.sliceTo(cv.commands[1].action_key, 0)); try testing.expectEqualStrings("text:ale bydle", std.mem.sliceTo(cv.commands[1].action, 0)); }
test "RepeatableCommand cval cleared" { const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Foo,action:ignore"); try testing.expectEqual(@as(usize, 1), list.cval().len);
try list.parseCLI(alloc, "clear"); try testing.expectEqual(@as(usize, 0), list.cval().len); }};
/// OSC 4, 10, 11, and 12 default color reporting format.pub const OSCColorReportFormat = enum { none, @"8-bit", @"16-bit",};
/// The default window theme.pub const WindowTheme = enum { auto, system, light, dark, ghostty,};
/// See window-colorspacepub const WindowColorspace = enum { srgb, @"display-p3",};
/// See macos-window-buttonspub const MacWindowButtons = enum { visible, hidden,};
/// See macos-titlebar-stylepub const MacTitlebarStyle = enum { native, transparent, tabs, hidden,};
/// See macos-titlebar-proxy-iconpub const MacTitlebarProxyIcon = enum { visible, hidden,};
/// See macos-hiddenpub const MacHidden = enum { never, always,};
/// See macos-icon////// Note: future versions of Ghostty can support a custom icon with/// path by changing this to a tagged union, which doesn't change our/// format at all.pub const MacAppIcon = enum { official, blueprint, chalkboard, microchip, glass, holographic, paper, retro, xray, custom, @"custom-style",};
/// See macos-icon-framepub const MacAppIconFrame = enum { aluminum, beige, plastic, chrome,};
/// See macos-shortcutspub const MacShortcuts = enum { allow, deny, ask,};
/// See gtk-single-instancepub const GtkSingleInstance = enum { false, true, detect,
pub const default: GtkSingleInstance = .detect;};
/// See gtk-tabs-locationpub const GtkTabsLocation = enum { top, bottom,};
/// See gtk-toolbar-stylepub const GtkToolbarStyle = enum { flat, raised, @"raised-border",};
/// See gtk-titlebar-stylepub const GtkTitlebarStyle = enum(c_int) { native, tabs,
pub const getGObjectType = switch (build_config.app_runtime) { .gtk => @import("gobject").ext.defineEnum( GtkTitlebarStyle, .{ .name = "GhosttyGtkTitlebarStyle" }, ),
.none => void, };};
/// See app-notificationspub const AppNotifications = packed struct { @"clipboard-copy": bool = true, @"config-reload": bool = true,};
/// See bell-featurespub const BellFeatures = packed struct { system: bool = false, audio: bool = false, attention: bool = true, title: bool = true, border: bool = false,};
/// See mouse-shift-capturepub const MouseShiftCapture = enum { false, true, always, never,};
/// See mouse-scroll-multiplierpub const MouseScrollMultiplier = struct { const Self = @This();
precision: f64 = 1, discrete: f64 = 3,
pub const default: MouseScrollMultiplier = .{};
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { const input = input_ orelse return error.ValueRequired; self.* = cli.args.parseAutoStruct( MouseScrollMultiplier, alloc, input, self.*, ) catch |err| switch (err) { error.InvalidValue => bare: { const v = std.fmt.parseFloat( f64, input, ) catch return error.InvalidValue; break :bare .{ .precision = v, .discrete = v, }; }, else => return err, }; }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { _ = alloc; return self.*; }
/// Compare if two of our value are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { return self.precision == other.precision and self.discrete == other.discrete; }
/// Used by Formatter pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [4096]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); writer.print( "precision:{d},discrete:{d}", .{ self.precision, self.discrete }, ) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, writer.buffered()); }
test "parse" { const testing = std.testing; const alloc = testing.allocator; const epsilon = 0.00001;
var args: Self = .{ .precision = 0.1, .discrete = 3 }; try args.parseCLI(alloc, "3"); try testing.expectApproxEqAbs(3, args.precision, epsilon); try testing.expectApproxEqAbs(3, args.discrete, epsilon);
args = .{ .precision = 0.1, .discrete = 3 }; try args.parseCLI(alloc, "precision:1"); try testing.expectApproxEqAbs(1, args.precision, epsilon); try testing.expectApproxEqAbs(3, args.discrete, epsilon);
args = .{ .precision = 0.1, .discrete = 3 }; try args.parseCLI(alloc, "discrete:5"); try testing.expectApproxEqAbs(0.1, args.precision, epsilon); try testing.expectApproxEqAbs(5, args.discrete, epsilon);
args = .{ .precision = 0.1, .discrete = 3 }; try args.parseCLI(alloc, "precision:3,discrete:7"); try testing.expectApproxEqAbs(3, args.precision, epsilon); try testing.expectApproxEqAbs(7, args.discrete, epsilon);
args = .{ .precision = 0.1, .discrete = 3 }; try args.parseCLI(alloc, "discrete:8,precision:6"); try testing.expectApproxEqAbs(6, args.precision, epsilon); try testing.expectApproxEqAbs(8, args.discrete, epsilon);
args = .default; try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "foo:1")); try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:bar")); try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,discrete:3,foo:5")); try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,,discrete:3")); try testing.expectError(error.InvalidValue, args.parseCLI(alloc, ",precision:1,discrete:3")); }
test "format entry MouseScrollMultiplier" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit();
var args: Self = .{ .precision = 1.5, .discrete = 2.5 }; try args.formatEntry(formatterpkg.entryFormatter("mouse-scroll-multiplier", &buf.writer)); try testing.expectEqualSlices(u8, "mouse-scroll-multiplier = precision:1.5,discrete:2.5\n", buf.written()); }};
/// How to treat requests to write to or read from the clipboardpub const ClipboardAccess = enum { allow, deny, ask,};
/// See window-save-statepub const WindowSaveState = enum { default, never, always,};
/// See window-new-tab-positionpub const WindowNewTabPosition = enum { current, end,};
/// See macos-dock-drop-behaviorpub const MacOSDockDropBehavior = enum { @"new-tab", @"new-window",};
/// See window-show-tab-barpub const WindowShowTabBar = enum { always, auto, never,};
/// See resize-overlaypub const ResizeOverlay = enum { always, never, @"after-first",};
/// See resize-overlay-positionpub const ResizeOverlayPosition = enum { center, @"top-left", @"top-center", @"top-right", @"bottom-left", @"bottom-center", @"bottom-right",};
/// See quick-terminal-positionpub const QuickTerminalPosition = enum { top, bottom, left, right, center,};
/// See quick-terminal-layerpub const QuickTerminalLayer = enum { overlay, top, bottom, background,};
/// See quick-terminal-sizepub const QuickTerminalSize = struct { primary: ?Size = null, secondary: ?Size = null,
pub const Size = union(enum) { percentage: f32, pixels: u32,
pub fn toPixels(self: Size, parent_dimensions: u32) u32 { switch (self) { .percentage => |v| { const dim: f32 = @floatFromInt(parent_dimensions); return @intFromFloat(v / 100.0 * dim); }, .pixels => |v| return v, } }
pub fn parse(input: []const u8) !Size { if (input.len == 0) return error.ValueRequired;
if (std.mem.endsWith(u8, input, "px")) { return .{ .pixels = std.fmt.parseInt( u32, input[0 .. input.len - "px".len], 10, ) catch return error.InvalidValue, }; }
if (std.mem.endsWith(u8, input, "%")) { const percentage = std.fmt.parseFloat( f32, input[0 .. input.len - "%".len], ) catch return error.InvalidValue;
if (percentage < 0) return error.InvalidValue; return .{ .percentage = percentage }; }
return error.MissingUnit; }
fn format(self: Size, writer: *std.Io.Writer) !void { switch (self) { .percentage => |v| try writer.print("{d}%", .{v}), .pixels => |v| try writer.print("{}px", .{v}), } } };
pub const Dimensions = struct { width: u32, height: u32, };
/// C API structure for QuickTerminalSize pub const C = extern struct { primary: C.Size, secondary: C.Size,
pub const Size = extern struct { tag: Tag, value: Value,
/// c_int because it needs to be extern compatible pub const Tag = enum(c_int) { none, percentage, pixels };
pub const Value = extern union { percentage: f32, pixels: u32, };
pub const none: C.Size = .{ .tag = .none, .value = undefined };
pub fn percentage(v: f32) C.Size { return .{ .tag = .percentage, .value = .{ .percentage = v }, }; }
pub fn pixels(v: u32) C.Size { return .{ .tag = .pixels, .value = .{ .pixels = v }, }; } }; };
pub fn cval(self: QuickTerminalSize) C { return .{ .primary = if (self.primary) |p| switch (p) { .percentage => |v| .percentage(v), .pixels => |v| .pixels(v), } else .none, .secondary = if (self.secondary) |s| switch (s) { .percentage => |v| .percentage(v), .pixels => |v| .pixels(v), } else .none, }; }
pub fn calculate( self: QuickTerminalSize, position: QuickTerminalPosition, dims: Dimensions, ) Dimensions { switch (position) { .left, .right => return .{ .width = if (self.primary) |v| v.toPixels(dims.width) else 400, .height = if (self.secondary) |v| v.toPixels(dims.height) else dims.height, }, .top, .bottom => return .{ .width = if (self.secondary) |v| v.toPixels(dims.width) else dims.width, .height = if (self.primary) |v| v.toPixels(dims.height) else 400, }, .center => if (dims.width >= dims.height) { return .{ .width = if (self.primary) |v| v.toPixels(dims.width) else 800, .height = if (self.secondary) |v| v.toPixels(dims.height) else 400, }; } else { return .{ .width = if (self.secondary) |v| v.toPixels(dims.width) else 400, .height = if (self.primary) |v| v.toPixels(dims.height) else 800, }; }, } }
pub fn parseCLI(self: *QuickTerminalSize, input: ?[]const u8) !void { const input_ = input orelse return error.ValueRequired; var it = std.mem.splitScalar(u8, input_, ',');
const primary = std.mem.trim( u8, it.next() orelse return error.ValueRequired, cli.args.whitespace, ); self.primary = try .parse(primary);
self.secondary = secondary: { const secondary = std.mem.trim( u8, it.next() orelse break :secondary null, cli.args.whitespace, ); break :secondary try .parse(secondary); };
if (it.next()) |_| return error.TooManyArguments; }
pub fn clone(self: *const QuickTerminalSize, _: Allocator) Allocator.Error!QuickTerminalSize { return .{ .primary = self.primary, .secondary = self.secondary, }; }
pub fn formatEntry(self: QuickTerminalSize, formatter: formatterpkg.EntryFormatter) !void { const primary = self.primary orelse return;
var buf: [4096]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf);
primary.format(&writer) catch return error.OutOfMemory; if (self.secondary) |secondary| { writer.writeByte(',') catch return error.OutOfMemory; secondary.format(&writer) catch return error.OutOfMemory; }
try formatter.formatEntry([]const u8, writer.buffered()); }
test "parse QuickTerminalSize" { const testing = std.testing; var v: QuickTerminalSize = undefined;
try v.parseCLI("50%"); try testing.expectEqual(50, v.primary.?.percentage); try testing.expectEqual(null, v.secondary);
try v.parseCLI("200px"); try testing.expectEqual(200, v.primary.?.pixels); try testing.expectEqual(null, v.secondary);
try v.parseCLI("50%,200px"); try testing.expectEqual(50, v.primary.?.percentage); try testing.expectEqual(200, v.secondary.?.pixels);
try testing.expectError(error.ValueRequired, v.parseCLI(null)); try testing.expectError(error.ValueRequired, v.parseCLI("")); try testing.expectError(error.ValueRequired, v.parseCLI("69px,")); try testing.expectError(error.TooManyArguments, v.parseCLI("69px,42%,69px"));
try testing.expectError(error.MissingUnit, v.parseCLI("420")); try testing.expectError(error.MissingUnit, v.parseCLI("bobr")); try testing.expectError(error.InvalidValue, v.parseCLI("bobr%")); try testing.expectError(error.InvalidValue, v.parseCLI("-32%")); try testing.expectError(error.InvalidValue, v.parseCLI("-69px")); } test "calculate QuickTerminalSize" { const testing = std.testing; const dims_landscape: Dimensions = .{ .width = 2560, .height = 1600 }; const dims_portrait: Dimensions = .{ .width = 1600, .height = 2560 };
{ const size: QuickTerminalSize = .{}; try testing.expectEqual( Dimensions{ .width = 2560, .height = 400 }, size.calculate(.top, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 400, .height = 1600 }, size.calculate(.left, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 800, .height = 400 }, size.calculate(.center, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 400, .height = 800 }, size.calculate(.center, dims_portrait), ); } { const size: QuickTerminalSize = .{ .primary = .{ .percentage = 20 } }; try testing.expectEqual( Dimensions{ .width = 2560, .height = 320 }, size.calculate(.top, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 512, .height = 1600 }, size.calculate(.left, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 512, .height = 400 }, size.calculate(.center, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 400, .height = 512 }, size.calculate(.center, dims_portrait), ); } { const size: QuickTerminalSize = .{ .primary = .{ .pixels = 600 } }; try testing.expectEqual( Dimensions{ .width = 2560, .height = 600 }, size.calculate(.top, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 600, .height = 1600 }, size.calculate(.left, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 600, .height = 400 }, size.calculate(.center, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 400, .height = 600 }, size.calculate(.center, dims_portrait), ); } { const size: QuickTerminalSize = .{ .primary = .{ .percentage = 69 }, .secondary = .{ .pixels = 420 }, }; try testing.expectEqual( Dimensions{ .width = 420, .height = 1104 }, size.calculate(.top, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 1766, .height = 420 }, size.calculate(.left, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 1766, .height = 420 }, size.calculate(.center, dims_landscape), ); try testing.expectEqual( Dimensions{ .width = 420, .height = 1766 }, size.calculate(.center, dims_portrait), ); } }};
/// See quick-terminal-screenpub const QuickTerminalScreen = enum { main, mouse, @"macos-menu-bar",};
// See quick-terminal-space-behaviorpub const QuickTerminalSpaceBehavior = enum { remain, move,};
/// See quick-terminal-keyboard-interactivitypub const QuickTerminalKeyboardInteractivity = enum { none, @"on-demand", exclusive,};
/// See grapheme-width-methodpub const GraphemeWidthMethod = enum { legacy, unicode,};
/// See alpha-blendingpub const AlphaBlending = enum { native, linear, @"linear-corrected",
pub fn isLinear(self: AlphaBlending) bool { return switch (self) { .native => false, .linear, .@"linear-corrected" => true, }; }};
/// See background-image-positionpub const BackgroundImagePosition = enum { @"top-left", @"top-center", @"top-right", @"center-left", @"center-center", @"center-right", @"bottom-left", @"bottom-center", @"bottom-right", center,};
/// See background-image-fitpub const BackgroundImageFit = enum { contain, cover, stretch, none,};
/// See freetype-load-flagpub const FreetypeLoadFlags = packed struct { // The defaults here at the time of writing this match the defaults // for Freetype itself. Ghostty hasn't made any opinionated changes // to these defaults. (Strictly speaking, `light` isn't FreeType's // own default, but appears to be the effective default with most // Fontconfig-aware software using FreeType, so until Ghostty // implements Fontconfig support we default to `light`.) hinting: bool = true, @"force-autohint": bool = false, monochrome: bool = false, autohint: bool = true, light: bool = true,};
/// See linux-cgrouppub const LinuxCgroup = enum { never, always, @"single-instance",};
/// See async-backendpub const AsyncBackend = enum { auto, epoll, io_uring,};
/// See auto-updatespub const AutoUpdate = enum { off, check, download,};
/// See background-blurpub const BackgroundBlur = union(enum) { false, true, @"macos-glass-regular", @"macos-glass-clear", radius: u8,
pub fn parseCLI(self: *BackgroundBlur, input: ?[]const u8) !void { const input_ = input orelse { // Emulate behavior for bools self.* = .true; return; };
// Try to parse normal bools if (cli.args.parseBool(input_)) |b| { self.* = if (b) .true else .false; return; } else |_| {}
// Try to parse enums if (std.meta.stringToEnum( std.meta.Tag(BackgroundBlur), input_, )) |v| switch (v) { inline else => |tag| tag: { // We can only parse void types const info = std.meta.fieldInfo(BackgroundBlur, tag); if (info.type != void) break :tag; self.* = @unionInit( BackgroundBlur, @tagName(tag), {}, ); return; }, };
self.* = .{ .radius = std.fmt.parseInt( u8, input_, 0, ) catch return error.InvalidValue }; }
pub fn enabled(self: BackgroundBlur) bool { return switch (self) { .false => false, .true => true, .radius => |v| v > 0,
// We treat these as true because they both imply some blur! // This has the effect of making the standard blur happen on // Linux. .@"macos-glass-regular", .@"macos-glass-clear" => true, }; }
pub fn cval(self: BackgroundBlur) i16 { return switch (self) { .false => 0, .true => 20, .radius => |v| v, // I hate sentinel values like this but this is only for // our macOS application currently. We can switch to a proper // tagged union if we ever need to. .@"macos-glass-regular" => -1, .@"macos-glass-clear" => -2, }; }
pub fn formatEntry( self: BackgroundBlur, formatter: anytype, ) !void { switch (self) { .false => try formatter.formatEntry(bool, false), .true => try formatter.formatEntry(bool, true), .radius => |v| try formatter.formatEntry(u8, v), .@"macos-glass-regular" => try formatter.formatEntry([]const u8, "macos-glass-regular"), .@"macos-glass-clear" => try formatter.formatEntry([]const u8, "macos-glass-clear"), } }
test "parse BackgroundBlur" { const testing = std.testing; var v: BackgroundBlur = undefined;
try v.parseCLI(null); try testing.expectEqual(.true, v);
try v.parseCLI("true"); try testing.expectEqual(.true, v);
try v.parseCLI("false"); try testing.expectEqual(.false, v);
try v.parseCLI("42"); try testing.expectEqual(42, v.radius);
try v.parseCLI("macos-glass-regular"); try testing.expectEqual(.@"macos-glass-regular", v);
try v.parseCLI("macos-glass-clear"); try testing.expectEqual(.@"macos-glass-clear", v);
try testing.expectError(error.InvalidValue, v.parseCLI("")); try testing.expectError(error.InvalidValue, v.parseCLI("aaaa")); try testing.expectError(error.InvalidValue, v.parseCLI("420")); }};
/// See window-decorationpub const WindowDecoration = enum(c_int) { auto, client, server, none,
/// Make this a valid gobject if we're in a GTK environment. pub const getGObjectType = switch (build_config.app_runtime) { .gtk => @import("gobject").ext.defineEnum( WindowDecoration, .{ .name = "GhosttyConfigWindowDecoration" }, ),
.none => void, };
pub fn parseCLI(input_: ?[]const u8) !WindowDecoration { const input = input_ orelse return .auto;
return if (cli.args.parseBool(input)) |b| if (b) .auto else .none else |_| if (std.meta.stringToEnum(WindowDecoration, input)) |v| v else error.InvalidValue; }
test "parse WindowDecoration" { const testing = std.testing;
{ const v = try WindowDecoration.parseCLI(null); try testing.expectEqual(WindowDecoration.auto, v); } { const v = try WindowDecoration.parseCLI("true"); try testing.expectEqual(WindowDecoration.auto, v); } { const v = try WindowDecoration.parseCLI("false"); try testing.expectEqual(WindowDecoration.none, v); } { const v = try WindowDecoration.parseCLI("server"); try testing.expectEqual(WindowDecoration.server, v); } { const v = try WindowDecoration.parseCLI("client"); try testing.expectEqual(WindowDecoration.client, v); } { const v = try WindowDecoration.parseCLI("auto"); try testing.expectEqual(WindowDecoration.auto, v); } { const v = try WindowDecoration.parseCLI("none"); try testing.expectEqual(WindowDecoration.none, v); } { try testing.expectError(error.InvalidValue, WindowDecoration.parseCLI("")); try testing.expectError(error.InvalidValue, WindowDecoration.parseCLI("aaaa")); } }};
/// See themepub const Theme = struct { light: []const u8, dark: []const u8,
pub fn parseCLI(self: *Theme, alloc: Allocator, input_: ?[]const u8) !void { const input = input_ orelse return error.ValueRequired; if (input.len == 0) return error.ValueRequired;
// If there is a comma, equal sign, or colon, then we assume that // we're parsing a light/dark mode theme pair. Note that "=" isn't // actually valid for setting a light/dark mode pair but I anticipate // it'll be a common typo. // // On Windows, a colon at index 1 is a drive letter (e.g. C:\...) // and should not trigger light/dark pair parsing. const has_colon = if (comptime builtin.os.tag == .windows) if (std.mem.indexOf(u8, input, ":")) |idx| idx != 1 else false else std.mem.indexOf(u8, input, ":") != null; if (std.mem.indexOf(u8, input, ",") != null or std.mem.indexOf(u8, input, "=") != null or has_colon) { self.* = try cli.args.parseAutoStruct( Theme, alloc, input, null, ); return; }
// Trim our value const trimmed = std.mem.trim(u8, input, cli.args.whitespace);
// Set the value to the specified value directly. self.* = .{ .light = try alloc.dupeZ(u8, trimmed), .dark = self.light, }; }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const Theme, alloc: Allocator) Allocator.Error!Theme { return .{ .light = try alloc.dupeZ(u8, self.light), .dark = try alloc.dupeZ(u8, self.dark), }; }
/// Used by Formatter pub fn formatEntry( self: Theme, formatter: anytype, ) !void { var buf: [4096]u8 = undefined; if (std.mem.eql(u8, self.light, self.dark)) { try formatter.formatEntry([]const u8, self.light); return; }
const str = std.fmt.bufPrint(&buf, "light:{s},dark:{s}", .{ self.light, self.dark, }) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, str); }
test "parse Theme" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
// Single { var v: Theme = undefined; try v.parseCLI(alloc, "foo"); try testing.expectEqualStrings("foo", v.light); try testing.expectEqualStrings("foo", v.dark); }
// Single whitespace { var v: Theme = undefined; try v.parseCLI(alloc, " foo "); try testing.expectEqualStrings("foo", v.light); try testing.expectEqualStrings("foo", v.dark); }
// Light/dark { var v: Theme = undefined; try v.parseCLI(alloc, " light:foo, dark : bar "); try testing.expectEqualStrings("foo", v.light); try testing.expectEqualStrings("bar", v.dark); }
var v: Theme = undefined; try testing.expectError(error.ValueRequired, v.parseCLI(alloc, null)); try testing.expectError(error.ValueRequired, v.parseCLI(alloc, "")); try testing.expectError(error.InvalidValue, v.parseCLI(alloc, "light:foo")); try testing.expectError(error.InvalidValue, v.parseCLI(alloc, "dark:foo")); }};
pub const Duration = struct { /// Duration in nanoseconds duration: u64 = 0,
const units = [_]struct { name: []const u8, factor: u64, }{ // The order is important as the first factor that matches will be the // default unit that is used for formatting. .{ .name = "y", .factor = 365 * std.time.ns_per_day }, .{ .name = "w", .factor = std.time.ns_per_week }, .{ .name = "d", .factor = std.time.ns_per_day }, .{ .name = "h", .factor = std.time.ns_per_hour }, .{ .name = "m", .factor = std.time.ns_per_min }, .{ .name = "s", .factor = std.time.ns_per_s }, .{ .name = "ms", .factor = std.time.ns_per_ms }, .{ .name = "µs", .factor = std.time.ns_per_us }, .{ .name = "us", .factor = std.time.ns_per_us }, .{ .name = "ns", .factor = 1 }, };
pub fn clone(self: *const Duration, _: Allocator) error{}!Duration { return .{ .duration = self.duration }; }
pub fn equal(self: Duration, other: Duration) bool { return self.duration == other.duration; }
pub fn round(self: Duration, to: u64) Duration { return .{ .duration = self.duration / to * to }; }
pub fn lte(self: Duration, other: Duration) bool { return self.duration <= other.duration; }
pub fn parseCLI(input: ?[]const u8) !Duration { var remaining = input orelse return error.ValueRequired;
var value: ?u64 = null; while (remaining.len > 0) { // Skip over whitespace before the number while (remaining.len > 0 and std.ascii.isWhitespace(remaining[0])) { remaining = remaining[1..]; }
// There was whitespace at the end, that's OK if (remaining.len == 0) break;
// Find the longest number const number: u64 = number: { var prev_number: ?u64 = null; var prev_remaining: ?[]const u8 = null; for (1..remaining.len + 1) |index| { prev_number = std.fmt.parseUnsigned(u64, remaining[0..index], 10) catch { if (prev_remaining) |prev| remaining = prev; break :number prev_number; }; prev_remaining = remaining[index..]; } if (prev_remaining) |prev| remaining = prev; break :number prev_number; } orelse return error.InvalidValue;
// A number without a unit is invalid unless the number is // exactly zero. In that case, the unit is unambiguous since // its all the same. if (remaining.len == 0) { if (number == 0) { value = 0; break; }
return error.InvalidValue; }
// Find the longest matching unit. Needs to be the longest matching // to distinguish 'm' from 'ms'. const factor = factor: { var prev_factor: ?u64 = null; var prev_index: ?usize = null; for (1..remaining.len + 1) |index| { const next_factor = next: { for (units) |unit| { if (std.mem.eql(u8, unit.name, remaining[0..index])) { break :next unit.factor; } } break :next null; }; if (next_factor) |next| { prev_factor = next; prev_index = index; } } if (prev_index) |index| { remaining = remaining[index..]; } break :factor prev_factor; } orelse return error.InvalidValue;
// Add our time value to the total. Avoid overflow with saturating math. const diff = std.math.mul(u64, number, factor) catch std.math.maxInt(u64); value = (value orelse 0) +| diff; }
return if (value) |v| .{ .duration = v } else error.ValueRequired; }
pub fn formatEntry(self: Duration, formatter: formatterpkg.EntryFormatter) !void { var buf: [64]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try self.format(&writer); try formatter.formatEntry([]const u8, writer.buffered()); }
pub fn format( self: Duration, writer: *std.Io.Writer, ) !void { var value = self.duration; var i: usize = 0; for (units) |unit| { if (value >= unit.factor) { if (i > 0) writer.writeAll(" ") catch unreachable; const remainder = value % unit.factor; const quotient = (value - remainder) / unit.factor; writer.print("{d}{s}", .{ quotient, unit.name }) catch unreachable; value = remainder; i += 1; } } }
pub fn cval(self: Duration) usize { return @intCast(self.asMilliseconds()); }
/// Convenience function to convert to milliseconds since many OS and /// library timing functions operate on that timescale. pub fn asMilliseconds(self: Duration) c_uint { const ms: u64 = std.math.divTrunc( u64, self.duration, std.time.ns_per_ms, ) catch std.math.maxInt(c_uint); return std.math.cast(c_uint, ms) orelse std.math.maxInt(c_uint); }};
pub const WindowPadding = struct { const Self = @This();
top_left: u32 = 0, bottom_right: u32 = 0,
pub fn clone(self: Self, _: Allocator) error{}!Self { return self; }
pub fn equal(self: Self, other: Self) bool { return std.meta.eql(self, other); }
pub fn parseCLI(input_: ?[]const u8) !WindowPadding { const input = input_ orelse return error.ValueRequired; const whitespace = " \t";
if (std.mem.indexOf(u8, input, ",")) |idx| { const input_left = std.mem.trim(u8, input[0..idx], whitespace); const input_right = std.mem.trim(u8, input[idx + 1 ..], whitespace); const left = std.fmt.parseInt(u32, input_left, 10) catch return error.InvalidValue; const right = std.fmt.parseInt(u32, input_right, 10) catch return error.InvalidValue; return .{ .top_left = left, .bottom_right = right }; } else { const value = std.fmt.parseInt( u32, std.mem.trim(u8, input, whitespace), 10, ) catch return error.InvalidValue; return .{ .top_left = value, .bottom_right = value }; } }
pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; if (self.top_left == self.bottom_right) { try formatter.formatEntry( []const u8, std.fmt.bufPrint( &buf, "{}", .{self.top_left}, ) catch return error.OutOfMemory, ); } else { try formatter.formatEntry( []const u8, std.fmt.bufPrint( &buf, "{},{}", .{ self.top_left, self.bottom_right }, ) catch return error.OutOfMemory, ); } }
test "parse WindowPadding" { const testing = std.testing;
{ const v = try WindowPadding.parseCLI("100"); try testing.expectEqual(WindowPadding{ .top_left = 100, .bottom_right = 100, }, v); }
{ const v = try WindowPadding.parseCLI("100,200"); try testing.expectEqual(WindowPadding{ .top_left = 100, .bottom_right = 200, }, v); }
// Trim whitespace { const v = try WindowPadding.parseCLI(" 100 , 200 "); try testing.expectEqual(WindowPadding{ .top_left = 100, .bottom_right = 200, }, v); }
try testing.expectError(error.ValueRequired, WindowPadding.parseCLI(null)); try testing.expectError(error.InvalidValue, WindowPadding.parseCLI("")); try testing.expectError(error.InvalidValue, WindowPadding.parseCLI("a")); }};
/// See scrollbarpub const Scrollbar = enum { system, never,};
/// See scroll-to-bottompub const ScrollToBottom = packed struct { keystroke: bool = true, output: bool = false,
pub const default: ScrollToBottom = .{};};
/// See notify-on-command-finishpub const NotifyOnCommandFinish = enum { never, unfocused, always,};
/// See notify-on-command-finish-actionpub const NotifyOnCommandFinishAction = packed struct { bell: bool = true, notify: bool = false,};
test "parse duration" { inline for (Duration.units) |unit| { var buf: [16]u8 = undefined; const t = try std.fmt.bufPrint(&buf, "0{s}", .{unit.name}); const d = try Duration.parseCLI(t); try std.testing.expectEqual(@as(u64, 0), d.duration); }
inline for (Duration.units) |unit| { var buf: [16]u8 = undefined; const t = try std.fmt.bufPrint(&buf, "1{s}", .{unit.name}); const d = try Duration.parseCLI(t); try std.testing.expectEqual(unit.factor, d.duration); }
{ const d = try Duration.parseCLI("0"); try std.testing.expectEqual(@as(u64, 0), d.duration); }
{ const d = try Duration.parseCLI("100ns"); try std.testing.expectEqual(@as(u64, 100), d.duration); }
{ const d = try Duration.parseCLI("1µs"); try std.testing.expectEqual(@as(u64, 1000), d.duration); }
{ const d = try Duration.parseCLI("1µs1ns"); try std.testing.expectEqual(@as(u64, 1001), d.duration); }
{ const d = try Duration.parseCLI("1µs 1ns"); try std.testing.expectEqual(@as(u64, 1001), d.duration); }
{ const d = try Duration.parseCLI(" 1µs1ns"); try std.testing.expectEqual(@as(u64, 1001), d.duration); }
{ const d = try Duration.parseCLI("1µs1ns "); try std.testing.expectEqual(@as(u64, 1001), d.duration); }
{ const d = try Duration.parseCLI("30s"); try std.testing.expectEqual(@as(u64, 30 * std.time.ns_per_s), d.duration); }
{ const d = try Duration.parseCLI("584y 49w 23h 34m 33s 709ms 551µs 615ns"); try std.testing.expectEqual(std.math.maxInt(u64), d.duration); }
// Overflow { const d = try Duration.parseCLI("600y"); try std.testing.expectEqual(std.math.maxInt(u64), d.duration); }
// Repeated units { const d = try Duration.parseCLI("100ns100ns"); try std.testing.expectEqual(@as(u64, 200), d.duration); }
try std.testing.expectError(error.ValueRequired, Duration.parseCLI(null)); try std.testing.expectError(error.ValueRequired, Duration.parseCLI("")); try std.testing.expectError(error.InvalidValue, Duration.parseCLI("1")); try std.testing.expectError(error.InvalidValue, Duration.parseCLI("s")); try std.testing.expectError(error.InvalidValue, Duration.parseCLI("1x")); try std.testing.expectError(error.InvalidValue, Duration.parseCLI("1 "));}
test "test format" { inline for (Duration.units) |unit| { const d: Duration = .{ .duration = unit.factor }; var actual_buf: [16]u8 = undefined; const actual = try std.fmt.bufPrint(&actual_buf, "{f}", .{d}); var expected_buf: [16]u8 = undefined; const expected = if (!std.mem.eql(u8, unit.name, "us")) try std.fmt.bufPrint(&expected_buf, "1{s}", .{unit.name}) else "1µs"; try std.testing.expectEqualSlices(u8, expected, actual); }}
test "test entryFormatter" { var buf: std.Io.Writer.Allocating = .init(std.testing.allocator); defer buf.deinit();
var p: Duration = .{ .duration = std.math.maxInt(u64) }; try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.written());}
const TestIterator = struct { data: []const []const u8, i: usize = 0,
pub fn next(self: *TestIterator) ?[]const u8 { if (self.i >= self.data.len) return null; const result = self.data[self.i]; self.i += 1; return result; }};
test "parse hook: invalid command" { const testing = std.testing; var cfg = try Config.default(testing.allocator); defer cfg.deinit(); const alloc = cfg._arena.?.allocator();
var it: TestIterator = .{ .data = &.{"foo"} }; try testing.expect(try cfg.parseManuallyHook(alloc, "--command", &it)); try testing.expect(cfg.command == null);}
test "parse e: command only" { const testing = std.testing; var cfg = try Config.default(testing.allocator); defer cfg.deinit(); const alloc = cfg._arena.?.allocator();
var it: TestIterator = .{ .data = &.{"foo"} }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
const cmd = cfg.@"initial-command".?; try testing.expect(cmd == .direct); try testing.expectEqual(cmd.direct.len, 1); try testing.expectEqualStrings(cmd.direct[0], "foo");}
test "parse e: command and args" { const testing = std.testing; var cfg = try Config.default(testing.allocator); defer cfg.deinit(); const alloc = cfg._arena.?.allocator();
var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
const cmd = cfg.@"initial-command".?; try testing.expect(cmd == .direct); try testing.expectEqual(cmd.direct.len, 3); try testing.expectEqualStrings(cmd.direct[0], "echo"); try testing.expectEqualStrings(cmd.direct[1], "foo"); try testing.expectEqualStrings(cmd.direct[2], "bar baz");}
test "clone default" { const testing = std.testing; const alloc = testing.allocator;
var source = try Config.default(alloc); defer source.deinit(); var dest = try source.clone(alloc); defer dest.deinit();
// Should have no changes var it = source.changeIterator(&dest); try testing.expectEqual(@as(?Key, null), it.next());
// I want to do this but this doesn't work (the API doesn't work) // try testing.expectEqualDeep(dest, source);}
test "clone preserves conditional state" { const testing = std.testing; const alloc = testing.allocator;
var a = try Config.default(alloc); defer a.deinit(); a._conditional_state.theme = .dark; try testing.expectEqual(.dark, a._conditional_state.theme); var dest = try a.clone(alloc); defer dest.deinit();
// Should have no changes var it = a.changeIterator(&dest); try testing.expectEqual(@as(?Key, null), it.next());
// Should have the same conditional state try testing.expectEqual(.dark, dest._conditional_state.theme);}
test "clone can then change conditional state" { // This tests a particular bug sequence where: // 1. Load light // 2. Convert to dark // 3. Clone dark // 4. Convert to light // 5. Config is still dark (bug) const testing = std.testing; const alloc = testing.allocator; var arena = ArenaAllocator.init(alloc); defer arena.deinit(); const alloc_arena = arena.allocator();
// Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme_light", .{}); defer file.close(); var writer = file.writer(&buf); try writer.interface.writeAll(@embedFile("testdata/theme_light")); try writer.end(); } { var file = try td.dir.createFile("theme_dark", .{}); defer file.close(); var writer = file.writer(&buf); try writer.interface.writeAll(@embedFile("testdata/theme_dark")); try writer.end(); } var light_buf: [std.fs.max_path_bytes]u8 = undefined; const light = try td.dir.realpath("theme_light", &light_buf); var dark_buf: [std.fs.max_path_bytes]u8 = undefined; const dark = try td.dir.realpath("theme_dark", &dark_buf);
var cfg_light = try Config.default(alloc); defer cfg_light.deinit(); var it: TestIterator = .{ .data = &.{ try std.fmt.allocPrint( alloc_arena, "--theme=light:{s},dark:{s}", .{ light, dark }, ), } }; try cfg_light.loadIter(alloc, &it); try cfg_light.finalize();
var cfg_dark = (try cfg_light.changeConditionalState(.{ .theme = .dark })).?; defer cfg_dark.deinit();
try testing.expectEqual(Color{ .r = 0xEE, .g = 0xEE, .b = 0xEE, }, cfg_dark.background);
var cfg_clone = try cfg_dark.clone(alloc); defer cfg_clone.deinit(); try testing.expectEqual(Color{ .r = 0xEE, .g = 0xEE, .b = 0xEE, }, cfg_clone.background);
var cfg_light2 = (try cfg_clone.changeConditionalState(.{ .theme = .light })).?; defer cfg_light2.deinit(); try testing.expectEqual(Color{ .r = 0xFF, .g = 0xFF, .b = 0xFF, }, cfg_light2.background);}
test "clone preserves conditional set" { const testing = std.testing; const alloc = testing.allocator;
var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--theme=light:foo,dark:bar", "--window-theme=auto", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
var clone1 = try cfg.clone(alloc); defer clone1.deinit();
try testing.expect(clone1._conditional_set.contains(.theme));}
test "working-directory expands tilde" { const testing = std.testing; const alloc = testing.allocator;
var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--working-directory=~/projects/ghostty", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
var buf: [std.fs.max_path_bytes]u8 = undefined; const expected = internal_os.expandHome( "~/projects/ghostty", &buf, ) catch "~/projects/ghostty"; try testing.expectEqualStrings(expected, cfg.@"working-directory".?.value().?);}
test "changed" { const testing = std.testing; const alloc = testing.allocator;
var source = try Config.default(alloc); defer source.deinit(); var dest = try source.clone(alloc); defer dest.deinit(); dest.@"font-thicken" = true;
try testing.expect(source.changed(&dest, .@"font-thicken")); try testing.expect(!source.changed(&dest, .@"font-size"));}
test "changeConditionalState ignores irrelevant changes" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--theme=foo", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expect(try cfg.changeConditionalState( .{ .theme = .dark }, ) == null); }}
test "changeConditionalState applies relevant changes" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--theme=light:foo,dark:bar", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
var cfg2 = (try cfg.changeConditionalState(.{ .theme = .dark })).?; defer cfg2.deinit();
try testing.expect(cfg2._conditional_set.contains(.theme)); }}test "theme loading" { const testing = std.testing; const alloc = testing.allocator; var arena = ArenaAllocator.init(alloc); defer arena.deinit(); const alloc_arena = arena.allocator();
// Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); var writer = file.writer(&buf); try writer.interface.writeAll(@embedFile("testdata/theme_simple")); try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf);
var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ try std.fmt.allocPrint(alloc_arena, "--theme={s}", .{path}), } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expectEqual(Color{ .r = 0x12, .g = 0x3A, .b = 0xBC, }, cfg.background);
// Not a conditional theme try testing.expect(!cfg._conditional_set.contains(.theme));}
test "theme loading preserves conditional state" { const testing = std.testing; const alloc = testing.allocator; var arena = ArenaAllocator.init(alloc); defer arena.deinit(); const alloc_arena = arena.allocator();
// Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); var writer = file.writer(&buf); try writer.interface.writeAll(@embedFile("testdata/theme_simple")); try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf);
var cfg = try Config.default(alloc); defer cfg.deinit(); cfg._conditional_state = .{ .theme = .dark }; var it: TestIterator = .{ .data = &.{ try std.fmt.allocPrint(alloc_arena, "--theme={s}", .{path}), } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expect(cfg._conditional_state.theme == .dark);}
test "theme priority is lower than config" { const testing = std.testing; const alloc = testing.allocator; var arena = ArenaAllocator.init(alloc); defer arena.deinit(); const alloc_arena = arena.allocator();
// Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); var writer = file.writer(&buf); try writer.interface.writeAll(@embedFile("testdata/theme_simple")); try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf);
var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--background=#ABCDEF", try std.fmt.allocPrint(alloc_arena, "--theme={s}", .{path}), } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expectEqual(Color{ .r = 0xAB, .g = 0xCD, .b = 0xEF, }, cfg.background);}
test "theme loading correct light/dark" { const testing = std.testing; const alloc = testing.allocator; var arena = ArenaAllocator.init(alloc); defer arena.deinit(); const alloc_arena = arena.allocator();
// Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme_light", .{}); defer file.close(); var writer = file.writer(&buf); try writer.interface.writeAll(@embedFile("testdata/theme_light")); try writer.end(); } { var file = try td.dir.createFile("theme_dark", .{}); defer file.close(); var writer = file.writer(&buf); try writer.interface.writeAll(@embedFile("testdata/theme_dark")); try writer.end(); } var light_buf: [std.fs.max_path_bytes]u8 = undefined; const light = try td.dir.realpath("theme_light", &light_buf); var dark_buf: [std.fs.max_path_bytes]u8 = undefined; const dark = try td.dir.realpath("theme_dark", &dark_buf);
// Light { var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ try std.fmt.allocPrint( alloc_arena, "--theme=light:{s},dark:{s}", .{ light, dark }, ), } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expectEqual(Color{ .r = 0xFF, .g = 0xFF, .b = 0xFF, }, cfg.background); }
// Dark { var cfg = try Config.default(alloc); defer cfg.deinit(); cfg._conditional_state = .{ .theme = .dark }; var it: TestIterator = .{ .data = &.{ try std.fmt.allocPrint( alloc_arena, "--theme=light:{s},dark:{s}", .{ light, dark }, ), } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expectEqual(Color{ .r = 0xEE, .g = 0xEE, .b = 0xEE, }, cfg.background); }
// Light to Dark { var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ try std.fmt.allocPrint( alloc_arena, "--theme=light:{s},dark:{s}", .{ light, dark }, ), } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
var new = (try cfg.changeConditionalState(.{ .theme = .dark })).?; defer new.deinit(); try testing.expectEqual(Color{ .r = 0xEE, .g = 0xEE, .b = 0xEE, }, new.background); }}
test "theme specifying light/dark changes window-theme from auto" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--theme=light:foo,dark:bar", "--window-theme=auto", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expect(cfg.@"window-theme" == .system); }}
test "theme specifying light/dark sets theme usage in conditional state" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--theme=light:foo,dark:bar", "--window-theme=auto", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expect(cfg.@"window-theme" == .system); try testing.expect(cfg._conditional_set.contains(.theme)); }}
test "compatibility: gtk-single-instance desktop" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--gtk-single-instance=desktop", } }; try cfg.loadIter(alloc, &it);
// We need to test this BEFORE finalize, because finalize will // convert our detect to a real value. try testing.expectEqual( GtkSingleInstance.detect, cfg.@"gtk-single-instance", ); }}
test "compatibility: removed cursor-invert-fg-bg" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--cursor-invert-fg-bg", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expectEqual( TerminalColor.@"cell-foreground", cfg.@"cursor-color", ); try testing.expectEqual( TerminalColor.@"cell-background", cfg.@"cursor-text", ); }}
test "compatibility: removed selection-invert-fg-bg" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--selection-invert-fg-bg", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expectEqual( TerminalColor.@"cell-background", cfg.@"selection-foreground", ); try testing.expectEqual( TerminalColor.@"cell-foreground", cfg.@"selection-background", ); }}
test "compatibility: removed bold-is-bright" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--bold-is-bright", } }; try cfg.loadIter(alloc, &it); try cfg.finalize();
try testing.expectEqual( BoldColor.bright, cfg.@"bold-color", ); }}
test "compatibility: window new-window" { const testing = std.testing; const alloc = testing.allocator;
{ var cfg = try Config.default(alloc); defer cfg.deinit(); var it: TestIterator = .{ .data = &.{ "--macos-dock-drop-behavior=window", } }; try cfg.loadIter(alloc, &it); try cfg.finalize(); try testing.expectEqual( MacOSDockDropBehavior.@"new-window", cfg.@"macos-dock-drop-behavior", ); }}