Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820#define _GNU_SOURCE#include "js-bindings.h"#include "version.h"#include "usb-midi.h"#include "recorder.h"#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <time.h>#include <math.h>#include <dirent.h>#include <unistd.h>#include <sys/stat.h>#include <sys/statvfs.h>#include <sys/wait.h>#include <sys/mount.h>#include <sys/reboot.h>#include <linux/reboot.h>#include <fcntl.h>#include <errno.h>#include <termios.h>#include "qrcodegen.h"#include <alsa/asoundlib.h>#include "machines.h"#ifdef HAVE_RAYLIB#include "raylib-soft.h"#endif
// Defined in ac-native.c — logs to USB mountextern void ac_log(const char *fmt, ...);extern void ac_log_flush(void);extern void ac_log_pause(void);extern void ac_log_resume(void);extern void perf_flush(void);
// Thread-local reference to current runtime (for C callbacks from JS)static __thread ACRuntime *current_rt = NULL;static __thread const char *current_phase = "boot";static int config_cache_dirty = 1; // reload /mnt/config.json when set
// Forward declaration (defined later in this file)static int resolve_color_name(const char *name, ACColor *out);static JSValue js_usb_midi_note_on(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);static JSValue js_usb_midi_note_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);static JSValue js_usb_midi_all_notes_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);
// ============================================================// QuickJS Class IDs for 3D objects// ============================================================static JSClassID form_class_id = 0;static JSClassID painting_class_id = 0;
// Form class finalizerstatic void js_form_finalizer(JSRuntime *rt, JSValue val) { (void)rt; ACForm *form = JS_GetOpaque(val, form_class_id); if (form) { free(form->positions); free(form->colors); free(form->tex_coords); free(form); }}
static JSClassDef form_class_def = { "Form", .finalizer = js_form_finalizer,};
// Painting class finalizerstatic void js_painting_finalizer(JSRuntime *rt, JSValue val) { (void)rt; ACFramebuffer *fb = JS_GetOpaque(val, painting_class_id); if (!fb) return; // Don't destroy runtime-owned nopaint buffers if (current_rt && (fb == current_rt->nopaint_painting || fb == current_rt->nopaint_buffer)) return; fb_destroy(fb);}
static JSClassDef painting_class_def = { "Painting", .finalizer = js_painting_finalizer,};
// ============================================================// Form constructor: new Form({type, positions, colors, texCoords}, {pos, rot, scale})// ============================================================
static JSValue js_form_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) { (void)new_target; if (argc < 1) return JS_EXCEPTION;
ACForm *form = calloc(1, sizeof(ACForm)); if (!form) return JS_EXCEPTION; form->scale = 1.0f;
// Parse geometry descriptor (first argument) JSValue geom = argv[0]; JSValue type_val = JS_GetPropertyStr(ctx, geom, "type"); if (!JS_IsUndefined(type_val)) { const char *type_str = JS_ToCString(ctx, type_val); if (type_str && strcmp(type_str, "line") == 0) form->type = FORM_TYPE_LINE; else form->type = FORM_TYPE_TRIANGLE; if (type_str) JS_FreeCString(ctx, type_str); } JS_FreeValue(ctx, type_val);
// Parse positions array: [[x,y,z,w], ...] JSValue pos_arr = JS_GetPropertyStr(ctx, geom, "positions"); if (JS_IsArray(ctx, pos_arr)) { JSValue len_val = JS_GetPropertyStr(ctx, pos_arr, "length"); int32_t len = 0; JS_ToInt32(ctx, &len, len_val); JS_FreeValue(ctx, len_val);
form->vert_count = len; form->positions = calloc(len * 4, sizeof(float));
for (int i = 0; i < len; i++) { JSValue vert = JS_GetPropertyUint32(ctx, pos_arr, i); for (int j = 0; j < 4; j++) { JSValue comp = JS_GetPropertyUint32(ctx, vert, j); double v = 0; JS_ToFloat64(ctx, &v, comp); form->positions[i * 4 + j] = (float)v; JS_FreeValue(ctx, comp); } JS_FreeValue(ctx, vert); } } JS_FreeValue(ctx, pos_arr);
// Parse colors array: [[r,g,b,a], ...] JSValue col_arr = JS_GetPropertyStr(ctx, geom, "colors"); if (JS_IsArray(ctx, col_arr)) { JSValue len_val = JS_GetPropertyStr(ctx, col_arr, "length"); int32_t len = 0; JS_ToInt32(ctx, &len, len_val); JS_FreeValue(ctx, len_val);
form->colors = calloc(len * 4, sizeof(float)); form->has_colors = 1;
for (int i = 0; i < len; i++) { JSValue col = JS_GetPropertyUint32(ctx, col_arr, i); for (int j = 0; j < 4; j++) { JSValue comp = JS_GetPropertyUint32(ctx, col, j); double v = 0; JS_ToFloat64(ctx, &v, comp); form->colors[i * 4 + j] = (float)v; JS_FreeValue(ctx, comp); } JS_FreeValue(ctx, col); } } JS_FreeValue(ctx, col_arr);
// Parse texCoords array: [[u,v], ...] JSValue tc_arr = JS_GetPropertyStr(ctx, geom, "texCoords"); if (JS_IsArray(ctx, tc_arr)) { JSValue len_val = JS_GetPropertyStr(ctx, tc_arr, "length"); int32_t len = 0; JS_ToInt32(ctx, &len, len_val); JS_FreeValue(ctx, len_val);
form->tex_coords = calloc(len * 2, sizeof(float)); for (int i = 0; i < len; i++) { JSValue uv = JS_GetPropertyUint32(ctx, tc_arr, i); for (int j = 0; j < 2; j++) { JSValue comp = JS_GetPropertyUint32(ctx, uv, j); double v = 0; JS_ToFloat64(ctx, &v, comp); form->tex_coords[i * 2 + j] = (float)v; JS_FreeValue(ctx, comp); } JS_FreeValue(ctx, uv); } } JS_FreeValue(ctx, tc_arr);
// Parse transform (second argument): {pos, rot, scale} if (argc >= 2 && JS_IsObject(argv[1])) { JSValue opts = argv[1];
JSValue pos = JS_GetPropertyStr(ctx, opts, "pos"); if (JS_IsArray(ctx, pos)) { for (int i = 0; i < 3; i++) { JSValue v = JS_GetPropertyUint32(ctx, pos, i); double d = 0; JS_ToFloat64(ctx, &d, v); form->position[i] = (float)d; JS_FreeValue(ctx, v); } } JS_FreeValue(ctx, pos);
JSValue rot = JS_GetPropertyStr(ctx, opts, "rot"); if (JS_IsArray(ctx, rot)) { for (int i = 0; i < 3; i++) { JSValue v = JS_GetPropertyUint32(ctx, rot, i); double d = 0; JS_ToFloat64(ctx, &d, v); form->rotation[i] = (float)d; JS_FreeValue(ctx, v); } } JS_FreeValue(ctx, rot);
JSValue sc = JS_GetPropertyStr(ctx, opts, "scale"); if (!JS_IsUndefined(sc)) { double d = 1.0; JS_ToFloat64(ctx, &d, sc); form->scale = (float)d; } JS_FreeValue(ctx, sc); }
// Create JS object with opaque pointer JSValue obj = JS_NewObjectClass(ctx, form_class_id); JS_SetOpaque(obj, form);
// Expose position and rotation as JS arrays (live references) JSValue js_pos = JS_NewArray(ctx); for (int i = 0; i < 3; i++) JS_SetPropertyUint32(ctx, js_pos, i, JS_NewFloat64(ctx, form->position[i])); JS_SetPropertyStr(ctx, obj, "position", js_pos);
JSValue js_rot = JS_NewArray(ctx); for (int i = 0; i < 3; i++) JS_SetPropertyUint32(ctx, js_rot, i, JS_NewFloat64(ctx, form->rotation[i])); JS_SetPropertyStr(ctx, obj, "rotation", js_rot);
JS_SetPropertyStr(ctx, obj, "noFade", JS_FALSE);
return obj;}
// ============================================================// Chain API: .form(f) renders a 3D form with current ink + camera// ============================================================
static JSValue js_chain_form(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { if (argc < 1) return JS_DupValue(ctx, this_val);
ACForm *form = JS_GetOpaque(argv[0], form_class_id); if (!form) return JS_DupValue(ctx, this_val);
ACRuntime *rt = current_rt; if (!rt) return JS_DupValue(ctx, this_val);
// Sync position/rotation from JS arrays back to C struct JSValue js_pos = JS_GetPropertyStr(ctx, argv[0], "position"); if (JS_IsArray(ctx, js_pos)) { for (int i = 0; i < 3; i++) { JSValue v = JS_GetPropertyUint32(ctx, js_pos, i); double d = 0; JS_ToFloat64(ctx, &d, v); form->position[i] = (float)d; JS_FreeValue(ctx, v); } } JS_FreeValue(ctx, js_pos);
JSValue js_rot = JS_GetPropertyStr(ctx, argv[0], "rotation"); if (JS_IsArray(ctx, js_rot)) { for (int i = 0; i < 3; i++) { JSValue v = JS_GetPropertyUint32(ctx, js_rot, i); double d = 0; JS_ToFloat64(ctx, &d, v); form->rotation[i] = (float)d; JS_FreeValue(ctx, v); } } JS_FreeValue(ctx, js_rot);
// Sync noFade JSValue nf = JS_GetPropertyStr(ctx, argv[0], "noFade"); form->no_fade = JS_ToBool(ctx, nf); JS_FreeValue(ctx, nf);
// Sync texture (painting with opaque ACFramebuffer*) JSValue tex = JS_GetPropertyStr(ctx, argv[0], "texture"); if (!JS_IsUndefined(tex) && !JS_IsNull(tex)) { ACFramebuffer *tex_fb = JS_GetOpaque(tex, painting_class_id); form->texture = tex_fb; } else { form->texture = NULL; } JS_FreeValue(ctx, tex);
// Ensure depth buffer exists if (!rt->depth_buf) { rt->depth_buf = depth_create(rt->graph->fb->width, rt->graph->fb->height, rt->graph->fb->stride); }
// Build view and projection matrices mat4 view, proj; camera3d_view_matrix(view, &rt->camera3d); float aspect = (float)rt->graph->fb->width / (float)rt->graph->fb->height; mat4_perspective(proj, 70.0f * (float)M_PI / 180.0f, aspect, 0.01f, 100.0f);
// Render graph3d_render_form(rt->graph->fb, rt->depth_buf, form, view, proj, rt->graph->ink, &rt->render_stats);
return JS_DupValue(ctx, this_val);}
// Chain .ink() — sets ink color, returns chain for further chainingstatic JSValue js_chain_ink(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ACGraph *g = current_rt->graph; ACColor c = {255, 255, 255, 255}; if (argc >= 3) { int r, gr, b; JS_ToInt32(ctx, &r, argv[0]); JS_ToInt32(ctx, &gr, argv[1]); JS_ToInt32(ctx, &b, argv[2]); c.r = (uint8_t)r; c.g = (uint8_t)gr; c.b = (uint8_t)b; if (argc >= 4) { int a; JS_ToInt32(ctx, &a, argv[3]); c.a = (uint8_t)a; } } else if (argc == 1) { if (JS_IsString(argv[0])) { const char *name = JS_ToCString(ctx, argv[0]); if (!resolve_color_name(name, &c)) c = (ACColor){255, 255, 255, 255}; JS_FreeCString(ctx, name); } else { int v; if (JS_ToInt32(ctx, &v, argv[0]) == 0) c = (ACColor){(uint8_t)v, (uint8_t)v, (uint8_t)v, 255}; } } graph_ink(g, c); return JS_DupValue(ctx, this_val);}
// Chain .write() — forward to existing write, return chainstatic JSValue js_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);static JSValue js_chain_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { js_write(ctx, JS_UNDEFINED, argc, argv); return JS_DupValue(ctx, this_val);}
// ============================================================// penLock() — enables FPS camera mode// ============================================================
static JSValue js_pen_lock(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (current_rt) { current_rt->pen_locked = 1; current_rt->fps_system_active = 1; camera3d_init(¤t_rt->camera3d); } return JS_UNDEFINED;}
// ============================================================// CUBEL geometry constant (wireframe cube, 12 lines = 24 vertices)// ============================================================
static JSValue build_cubel(JSContext *ctx) { // 12 edges of a unit cube from -1 to 1 static const float cube_lines[24][4] = { // Bottom face {-1,-1,-1,1}, { 1,-1,-1,1}, { 1,-1,-1,1}, { 1,-1, 1,1}, { 1,-1, 1,1}, {-1,-1, 1,1}, {-1,-1, 1,1}, {-1,-1,-1,1}, // Top face {-1, 1,-1,1}, { 1, 1,-1,1}, { 1, 1,-1,1}, { 1, 1, 1,1}, { 1, 1, 1,1}, {-1, 1, 1,1}, {-1, 1, 1,1}, {-1, 1,-1,1}, // Vertical edges {-1,-1,-1,1}, {-1, 1,-1,1}, { 1,-1,-1,1}, { 1, 1,-1,1}, { 1,-1, 1,1}, { 1, 1, 1,1}, {-1,-1, 1,1}, {-1, 1, 1,1}, };
JSValue geom = JS_NewObject(ctx); JS_SetPropertyStr(ctx, geom, "type", JS_NewString(ctx, "line"));
JSValue positions = JS_NewArray(ctx); for (int i = 0; i < 24; i++) { JSValue vert = JS_NewArray(ctx); for (int j = 0; j < 4; j++) JS_SetPropertyUint32(ctx, vert, j, JS_NewFloat64(ctx, cube_lines[i][j])); JS_SetPropertyUint32(ctx, positions, i, vert); } JS_SetPropertyStr(ctx, geom, "positions", positions);
return geom;}
// QUAD geometry constant (2 triangles = 6 vertices with gradient colors)static JSValue build_quad(JSContext *ctx) { static const float quad_pos[6][4] = { {-1,-1,0,1}, {-1,1,0,1}, {1,1,0,1}, // tri 1 {-1,-1,0,1}, {1,1,0,1}, {1,-1,0,1}, // tri 2 }; static const float quad_col[6][4] = { {1,0,0,1}, {0,1,0,1}, {0,0,1,1}, {1,0,0,1}, {0,0,1,1}, {1,1,0,1}, };
JSValue geom = JS_NewObject(ctx); JS_SetPropertyStr(ctx, geom, "type", JS_NewString(ctx, "triangle"));
JSValue positions = JS_NewArray(ctx); JSValue colors = JS_NewArray(ctx); for (int i = 0; i < 6; i++) { JSValue vert = JS_NewArray(ctx); JSValue col = JS_NewArray(ctx); for (int j = 0; j < 4; j++) { JS_SetPropertyUint32(ctx, vert, j, JS_NewFloat64(ctx, quad_pos[i][j])); JS_SetPropertyUint32(ctx, col, j, JS_NewFloat64(ctx, quad_col[i][j])); } JS_SetPropertyUint32(ctx, positions, i, vert); JS_SetPropertyUint32(ctx, colors, i, col); } JS_SetPropertyStr(ctx, geom, "positions", positions); JS_SetPropertyStr(ctx, geom, "colors", colors);
return geom;}
// ============================================================// JS Native Functions — Graphics// ============================================================
// Resolve a CSS color name string to an ACColor. Returns 1 on match, 0 if unknown.static int resolve_color_name(const char *name, ACColor *out) { if (!name) return 0; if (strcmp(name, "black") == 0) { *out = (ACColor){0, 0, 0, 255}; return 1; } if (strcmp(name, "white") == 0) { *out = (ACColor){255, 255, 255, 255}; return 1; } if (strcmp(name, "red") == 0) { *out = (ACColor){255, 0, 0, 255}; return 1; } if (strcmp(name, "green") == 0) { *out = (ACColor){0, 128, 0, 255}; return 1; } if (strcmp(name, "blue") == 0) { *out = (ACColor){0, 0, 255, 255}; return 1; } if (strcmp(name, "yellow") == 0) { *out = (ACColor){255, 255, 0, 255}; return 1; } if (strcmp(name, "cyan") == 0) { *out = (ACColor){0, 255, 255, 255}; return 1; } if (strcmp(name, "magenta") == 0) { *out = (ACColor){255, 0, 255, 255}; return 1; } if (strcmp(name, "gray") == 0) { *out = (ACColor){128, 128, 128, 255}; return 1; } if (strcmp(name, "grey") == 0) { *out = (ACColor){128, 128, 128, 255}; return 1; } if (strcmp(name, "orange") == 0) { *out = (ACColor){255, 165, 0, 255}; return 1; } if (strcmp(name, "purple") == 0) { *out = (ACColor){128, 0, 128, 255}; return 1; } if (strcmp(name, "pink") == 0) { *out = (ACColor){255, 192, 203, 255}; return 1; } if (strcmp(name, "lime") == 0) { *out = (ACColor){0, 255, 0, 255}; return 1; } if (strcmp(name, "brown") == 0) { *out = (ACColor){139, 69, 19, 255}; return 1; } return 0;}
static JSValue js_wipe(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACGraph *g = current_rt->graph; ACColor c = {0, 0, 0, 255}; if (argc == 0) { c = (ACColor){255, 255, 255, 255}; } else if (argc >= 3) { int r, gr, b; JS_ToInt32(ctx, &r, argv[0]); JS_ToInt32(ctx, &gr, argv[1]); JS_ToInt32(ctx, &b, argv[2]); c = (ACColor){(uint8_t)r, (uint8_t)gr, (uint8_t)b, 255}; } else if (argc == 1) { if (JS_IsString(argv[0])) { const char *name = JS_ToCString(ctx, argv[0]); if (!resolve_color_name(name, &c)) c = (ACColor){0, 0, 0, 255}; // unknown name → black JS_FreeCString(ctx, name); } else { int v; if (JS_ToInt32(ctx, &v, argv[0]) == 0) { c = (ACColor){(uint8_t)v, (uint8_t)v, (uint8_t)v, 255}; } } } graph_wipe(g, c);
// Clear depth buffer for new frame if (current_rt && current_rt->depth_buf) depth_clear(current_rt->depth_buf); // Reset render stats if (current_rt) memset(¤t_rt->render_stats, 0, sizeof(ACRenderStats));
// Return chain object for .form()/.ink() chaining JSValue global = JS_GetGlobalObject(ctx); JSValue chain = JS_GetPropertyStr(ctx, global, "__paintApi"); JS_FreeValue(ctx, global); return chain;}
static JSValue js_ink(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACGraph *g = current_rt->graph; ACColor c = {255, 255, 255, 255}; if (argc >= 3) { int r, gr, b; JS_ToInt32(ctx, &r, argv[0]); JS_ToInt32(ctx, &gr, argv[1]); JS_ToInt32(ctx, &b, argv[2]); c.r = (uint8_t)r; c.g = (uint8_t)gr; c.b = (uint8_t)b; if (argc >= 4) { int a; JS_ToInt32(ctx, &a, argv[3]); c.a = (uint8_t)a; } } else if (argc == 1) { if (JS_IsString(argv[0])) { const char *name = JS_ToCString(ctx, argv[0]); if (!resolve_color_name(name, &c)) c = (ACColor){255, 255, 255, 255}; // unknown name → white JS_FreeCString(ctx, name); } else { int v; if (JS_ToInt32(ctx, &v, argv[0]) == 0) { c = (ACColor){(uint8_t)v, (uint8_t)v, (uint8_t)v, 255}; } } } graph_ink(g, c);
JSValue global = JS_GetGlobalObject(ctx); JSValue paint_api = JS_GetPropertyStr(ctx, global, "__paintApi"); JS_FreeValue(ctx, global); return paint_api;}
static JSValue js_plot(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2) return JS_UNDEFINED; int x, y; JS_ToInt32(ctx, &x, argv[0]); JS_ToInt32(ctx, &y, argv[1]); graph_plot(current_rt->graph, x, y); return JS_UNDEFINED;}
static JSValue js_line(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 4) return JS_UNDEFINED; int x0, y0, x1, y1; JS_ToInt32(ctx, &x0, argv[0]); JS_ToInt32(ctx, &y0, argv[1]); JS_ToInt32(ctx, &x1, argv[2]); JS_ToInt32(ctx, &y1, argv[3]); if (argc >= 5) { int thickness; JS_ToInt32(ctx, &thickness, argv[4]); graph_line_thick(current_rt->graph, x0, y0, x1, y1, thickness); } else { graph_line(current_rt->graph, x0, y0, x1, y1); } return JS_UNDEFINED;}
static JSValue js_box(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 4) return JS_UNDEFINED; int x, y, w, h; JS_ToInt32(ctx, &x, argv[0]); JS_ToInt32(ctx, &y, argv[1]); JS_ToInt32(ctx, &w, argv[2]); JS_ToInt32(ctx, &h, argv[3]);
int filled = 1; if (argc >= 5 && JS_IsString(argv[4])) { const char *mode = JS_ToCString(ctx, argv[4]); if (mode && strcmp(mode, "outline") == 0) filled = 0; JS_FreeCString(ctx, mode); } graph_box(current_rt->graph, x, y, w, h, filled);
JSValue global = JS_GetGlobalObject(ctx); JSValue paint_api = JS_GetPropertyStr(ctx, global, "__paintApi"); JS_FreeValue(ctx, global); return paint_api;}
static JSValue js_circle(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 3) return JS_UNDEFINED; int cx, cy, r; JS_ToInt32(ctx, &cx, argv[0]); JS_ToInt32(ctx, &cy, argv[1]); JS_ToInt32(ctx, &r, argv[2]); int filled = (argc >= 4) ? JS_ToBool(ctx, argv[3]) : 0; graph_circle(current_rt->graph, cx, cy, r, filled); return JS_UNDEFINED;}
static JSValue js_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_UNDEFINED; const char *text = JS_ToCString(ctx, argv[0]); if (!text) return JS_UNDEFINED;
int x = 0, y = 0, scale = 1;
if (argc >= 2) { if (JS_IsObject(argv[1])) { JSValue vx = JS_GetPropertyStr(ctx, argv[1], "x"); JSValue vy = JS_GetPropertyStr(ctx, argv[1], "y"); if (!JS_IsUndefined(vx)) JS_ToInt32(ctx, &x, vx); if (!JS_IsUndefined(vy)) JS_ToInt32(ctx, &y, vy);
JSValue center = JS_GetPropertyStr(ctx, argv[1], "center"); if (JS_IsString(center)) { const char *cv = JS_ToCString(ctx, center); if (cv) { int tw = font_measure(text, scale); if (strchr(cv, 'x')) x = (current_rt->graph->fb->width - tw) / 2; if (strchr(cv, 'y')) y = (current_rt->graph->fb->height - FONT_CHAR_H * scale) / 2; JS_FreeCString(ctx, cv); } } JS_FreeValue(ctx, center);
JSValue size = JS_GetPropertyStr(ctx, argv[1], "size"); if (!JS_IsUndefined(size)) JS_ToInt32(ctx, &scale, size); JS_FreeValue(ctx, size);
JS_FreeValue(ctx, vx); JS_FreeValue(ctx, vy); } }
// Check for font option: "matrix"/"MatrixChunky8", "unifont", // "font_1"/"6x10" (default). Web pieces also pass the font as the 6th // positional arg — write(text, pos, bg, bounds, wordWrap, font) — so // honor that form too (nom's word cells select "unifont" this way). int font_id = FONT_6X10; const char *fname = NULL; if (argc >= 6 && JS_IsString(argv[5])) fname = JS_ToCString(ctx, argv[5]); JSValue font_v = JS_UNDEFINED; if (!fname && argc >= 2 && JS_IsObject(argv[1])) { font_v = JS_GetPropertyStr(ctx, argv[1], "font"); if (JS_IsString(font_v)) fname = JS_ToCString(ctx, font_v); } if (fname) { if (strcmp(fname, "matrix") == 0 || strcmp(fname, "MatrixChunky8") == 0) font_id = FONT_MATRIX; else if (strcmp(fname, "unifont") == 0) font_id = FONT_UNIFONT; else if (strcmp(fname, "font_1") == 0 || strcmp(fname, "6x10") == 0) font_id = FONT_6X10; JS_FreeCString(ctx, fname); } JS_FreeValue(ctx, font_v);
// Re-check center with correct font metrics for non-8x8 fonts if (font_id != FONT_8X8 && argc >= 2 && JS_IsObject(argv[1])) { JSValue center2 = JS_GetPropertyStr(ctx, argv[1], "center"); if (JS_IsString(center2)) { const char *cv = JS_ToCString(ctx, center2); if (cv) { int tw = (font_id == FONT_MATRIX) ? font_measure_matrix(text, scale) : (font_id == FONT_UNIFONT) ? font_measure_unifont(text, scale) : font_measure_6x10(text, scale); int th = (font_id == FONT_MATRIX) ? 8 * scale // MatrixChunky8 ascent : (font_id == FONT_UNIFONT) ? FONT_UNIFONT_H * scale : FONT_6X10_CHAR_H * scale; if (strchr(cv, 'x')) x = (current_rt->graph->fb->width - tw) / 2; if (strchr(cv, 'y')) y = (current_rt->graph->fb->height - th) / 2; JS_FreeCString(ctx, cv); } } JS_FreeValue(ctx, center2); }
if (font_id == FONT_MATRIX) font_draw_matrix(current_rt->graph, text, x, y, scale); else if (font_id == FONT_UNIFONT) font_draw_unifont(current_rt->graph, text, x, y, scale); else if (font_id == FONT_6X10) font_draw_6x10(current_rt->graph, text, x, y, scale); else font_draw(current_rt->graph, text, x, y, scale); JS_FreeCString(ctx, text); return JS_UNDEFINED;}
static JSValue js_scroll(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; int dx = 0, dy = 0; if (argc >= 1) JS_ToInt32(ctx, &dx, argv[0]); if (argc >= 2) JS_ToInt32(ctx, &dy, argv[1]); graph_scroll(current_rt->graph, dx, dy); return JS_UNDEFINED;}
static JSValue js_blur(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; int s = 1; if (argc >= 1) JS_ToInt32(ctx, &s, argv[0]); graph_blur(current_rt->graph, s); return JS_UNDEFINED;}
static JSValue js_zoom(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; double level = 1.0; if (argc >= 1) JS_ToFloat64(ctx, &level, argv[0]); graph_zoom(current_rt->graph, level); return JS_UNDEFINED;}
static JSValue js_contrast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; double level = 1.0; if (argc >= 1) JS_ToFloat64(ctx, &level, argv[0]); graph_contrast(current_rt->graph, level); return JS_UNDEFINED;}
static JSValue js_spin(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; double angle = 0.0; if (argc >= 1) JS_ToFloat64(ctx, &angle, argv[0]); graph_spin(current_rt->graph, angle); return JS_UNDEFINED;}
// qr(text, x, y, scale) — render QR code at positionstatic JSValue js_qr(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 3) return JS_UNDEFINED; const char *text = JS_ToCString(ctx, argv[0]); if (!text) return JS_UNDEFINED; int x = 0, y = 0, scale = 2; JS_ToInt32(ctx, &x, argv[1]); JS_ToInt32(ctx, &y, argv[2]); if (argc >= 4) JS_ToInt32(ctx, &scale, argv[3]); graph_qr(current_rt->graph, text, x, y, scale); JS_FreeCString(ctx, text); return JS_UNDEFINED;}
// ============================================================// JS Native Functions — System// ============================================================
static JSValue js_console_log(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; // Build message string, then log via ac_log (writes to both stderr + log file) char buf[512]; int pos = 0; for (int i = 0; i < argc && pos < (int)sizeof(buf) - 2; i++) { const char *s = JS_ToCString(ctx, argv[i]); if (s) { int n = snprintf(buf + pos, sizeof(buf) - pos, "%s%s", i ? " " : "", s); if (n > 0) pos += n; JS_FreeCString(ctx, s); } } buf[pos] = '\0'; ac_log("[js] %s\n", buf); return JS_UNDEFINED;}
static JSValue js_performance_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); double ms = ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; return JS_NewFloat64(ctx, ms);}
// No-op function for stubsstatic JSValue js_noop(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)ctx; (void)this_val; (void)argc; (void)argv; return JS_UNDEFINED;}
// clock.time() — returns a JS Date object with the current system timestatic JSValue js_clock_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); double ms = (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1000000.0; JSValue global = JS_GetGlobalObject(ctx); JSValue date_ctor = JS_GetPropertyStr(ctx, global, "Date"); JSValue ms_val = JS_NewFloat64(ctx, ms); JSValue date = JS_CallConstructor(ctx, date_ctor, 1, &ms_val); JS_FreeValue(ctx, ms_val); JS_FreeValue(ctx, date_ctor); JS_FreeValue(ctx, global); return date;}
// Returns a resolved promise with nullstatic JSValue js_promise_null(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; // Use Promise.resolve(null) JSValue global = JS_GetGlobalObject(ctx); JSValue promise = JS_GetPropertyStr(ctx, global, "Promise"); JSValue resolve = JS_GetPropertyStr(ctx, promise, "resolve"); JSValue null_val = JS_NULL; JSValue result = JS_Call(ctx, resolve, promise, 1, &null_val); JS_FreeValue(ctx, resolve); JS_FreeValue(ctx, promise); JS_FreeValue(ctx, global); return result;}
// Returns a function that returns a no-op object with send()static JSValue js_net_udp(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; JSValue obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, obj, "send", JS_NewCFunction(ctx, js_noop, "send", 2)); return obj;}
// ============================================================// Sound Bindings// ============================================================
static WaveType parse_wave_type(const char *type) { if (!type) return WAVE_SINE; if (strcmp(type, "sine") == 0) return WAVE_SINE; if (strcmp(type, "triangle") == 0) return WAVE_TRIANGLE; if (strcmp(type, "sawtooth") == 0) return WAVE_SAWTOOTH; if (strcmp(type, "square") == 0) return WAVE_SQUARE; if (strcmp(type, "noise-white") == 0 || strcmp(type, "noise") == 0) return WAVE_NOISE; if (strcmp(type, "whistle") == 0 || strcmp(type, "ocarina") == 0 || strcmp(type, "flute") == 0 || strcmp(type, "skullwhistle") == 0 || strcmp(type, "skull-whistle") == 0) return WAVE_WHISTLE; // Karplus-Strong plucked string (harp / guitar / pluck). See // audio.c:generate_harp_sample for algorithm + citations. if (strcmp(type, "harp") == 0 || strcmp(type, "pluck") == 0 || strcmp(type, "guitar") == 0 || strcmp(type, "string") == 0) return WAVE_HARP; // Modal-additive grand piano with stretched-harmonic partials. See // audio.c:generate_piano_sample for algorithm + citations. if (strcmp(type, "piano") == 0 || strcmp(type, "grand") == 0) return WAVE_PIANO; if (strncmp(type, "gun", 3) == 0) return WAVE_GUN; // Unknown / legacy types fall back to sine. return WAVE_SINE;}
// Parse the preset suffix for a gun-* wave type string. Optional model// suffix `/classic` or `/physical` overrides the preset's default model.// Examples: "gun-pistol", "gun-pistol/classic", "gun-pistol/physical".// Returns GUN_PISTOL on parse failure. Pass `out_force_model` to receive// the override (-1 = use preset default, 0 = CLASSIC, 1 = PHYSICAL).static GunPreset parse_gun_preset(const char *type, int *out_force_model) { if (out_force_model) *out_force_model = -1; if (!type) return GUN_PISTOL; if (strncmp(type, "gun", 3) != 0) return GUN_PISTOL; const char *p = type + 3; if (*p == '-' || *p == '_') p++; if (!*p) return GUN_PISTOL;
// Split off /classic or /physical suffix into a temp buffer. char name_buf[32]; const char *slash = strchr(p, '/'); if (slash && out_force_model) { size_t n = (size_t)(slash - p); if (n >= sizeof(name_buf)) n = sizeof(name_buf) - 1; memcpy(name_buf, p, n); name_buf[n] = '\0'; p = name_buf; const char *m = slash + 1; if (strcmp(m, "classic") == 0) *out_force_model = 0; else if (strcmp(m, "physical") == 0 || strcmp(m, "phys") == 0) *out_force_model = 1; }
if (strcmp(p, "pistol") == 0) return GUN_PISTOL; if (strcmp(p, "rifle") == 0) return GUN_RIFLE; if (strcmp(p, "shotgun") == 0) return GUN_SHOTGUN; if (strcmp(p, "smg") == 0) return GUN_SMG; if (strcmp(p, "suppressed") == 0 || strcmp(p, "silenced") == 0) return GUN_SUPPRESSED; if (strcmp(p, "lmg") == 0 || strcmp(p, "mg") == 0) return GUN_LMG; if (strcmp(p, "sniper") == 0) return GUN_SNIPER; if (strcmp(p, "grenade") == 0) return GUN_GRENADE; if (strcmp(p, "rpg") == 0 || strcmp(p, "rocket") == 0) return GUN_RPG; if (strcmp(p, "reload") == 0) return GUN_RELOAD; if (strcmp(p, "cock") == 0 || strcmp(p, "bolt") == 0) return GUN_COCK; if (strcmp(p, "ricochet") == 0 || strcmp(p, "pew") == 0) return GUN_RICOCHET; return GUN_PISTOL;}
// synthObj.kill(fade) — method on synth return objectstatic JSValue js_synth_obj_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ACAudio *audio = current_rt->audio; if (!audio) return JS_UNDEFINED;
// Get id from 'this' object JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); double id_d = 0; if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); JS_FreeValue(ctx, id_v); uint64_t id = (uint64_t)id_d; if (id == 0) return JS_UNDEFINED;
double fade = 0.025; if (argc >= 1 && JS_IsNumber(argv[0])) { JS_ToFloat64(ctx, &fade, argv[0]); } audio_kill(audio, id, fade); return JS_UNDEFINED;}
// synthObj.update({volume, tone, pan}) — method on synth return objectstatic JSValue js_synth_obj_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ACAudio *audio = current_rt->audio; if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED;
// Get id from 'this' object JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); double id_d = 0; if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); JS_FreeValue(ctx, id_v); uint64_t id = (uint64_t)id_d; if (id == 0) return JS_UNDEFINED;
double freq = -1, vol = -1, pan = -3; JSValue v; v = JS_GetPropertyStr(ctx, argv[0], "tone"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[0], "volume"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[0], "pan"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); JS_FreeValue(ctx, v);
audio_update(audio, id, freq, vol, pan); return JS_UNDEFINED;}
// sound.synth({type, tone, duration, volume, attack, decay, pan})static JSValue js_synth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED;
JSValue opts = argv[0];
// Parse type. For "gun-*" strings we also extract the preset so the // DWG synth can branch to audio_synth_gun below. Optional "/classic" // or "/physical" suffix overrides the preset's default model. WaveType wt = WAVE_SINE; GunPreset gun_preset = GUN_PISTOL; int gun_force_model = -1; int is_gun = 0; JSValue type_v = JS_GetPropertyStr(ctx, opts, "type"); if (JS_IsString(type_v)) { const char *ts = JS_ToCString(ctx, type_v); wt = parse_wave_type(ts); if (wt == WAVE_GUN) { gun_preset = parse_gun_preset(ts, &gun_force_model); is_gun = 1; } JS_FreeCString(ctx, ts); } JS_FreeValue(ctx, type_v);
// Parse tone (can be number or string) double freq = 440.0; JSValue tone_v = JS_GetPropertyStr(ctx, opts, "tone"); if (JS_IsNumber(tone_v)) { JS_ToFloat64(ctx, &freq, tone_v); } else if (JS_IsString(tone_v)) { const char *ts = JS_ToCString(ctx, tone_v); freq = audio_note_to_freq(ts); JS_FreeCString(ctx, ts); } JS_FreeValue(ctx, tone_v);
// Parse duration (number or "🔁" for infinity) double duration = 0.1; JSValue dur_v = JS_GetPropertyStr(ctx, opts, "duration"); if (JS_IsNumber(dur_v)) { JS_ToFloat64(ctx, &duration, dur_v); } else if (JS_IsString(dur_v)) { duration = INFINITY; } JS_FreeValue(ctx, dur_v);
// Parse volume, attack, decay, pan double volume = 1.0, attack = 0.005, decay = 0.1, pan = 0.0; JSValue v;
v = JS_GetPropertyStr(ctx, opts, "volume"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &volume, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "attack"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &attack, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "decay"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &decay, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "pan"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); JS_FreeValue(ctx, v);
// Optional GM-synthesis program (0-127). When present and >= 0 we try the // bespoke algorithmic GM voice first; if that program is implemented // (Piano family 0-7 in this slice) it wins and `type` is ignored. If not, // audio_synth_gm returns 0 and we fall through to the `type`-based path so // unimplemented programs still use notepat's family-recipe wave. // docs/gm-synthesis/01-piano-mallet-organ-guitar.md int gm_program = -1; JSValue gmp_v = JS_GetPropertyStr(ctx, opts, "gmProgram"); if (JS_IsNumber(gmp_v)) { double gd; if (JS_ToFloat64(ctx, &gd, gmp_v) == 0) gm_program = (int)gd; } JS_FreeValue(ctx, gmp_v);
// Create voice. Guns take a separate path so we can seed per-preset // DWG parameters (bore length, body modes, etc). uint64_t id; if (gm_program >= 0 && !is_gun && (id = audio_synth_gm(audio, gm_program, freq, duration, volume, attack, decay, pan)) != 0) { ac_log("[synth] gm program=%d freq=%.1f vol=%.2f dur=%.1f id=%lu\n", gm_program, freq, volume, duration, (unsigned long)id); } else if (is_gun) { // Let `tone` (1.0 default) scale the combustion pressure so pads // can express accent via velocity. A JS caller passing tone=1.0 // = unity pressure from the preset. double pressure_scale = freq > 0 && freq < 5.0 ? freq : 1.0; id = audio_synth_gun(audio, gun_preset, duration, volume, attack, decay, pan, pressure_scale, gun_force_model); fprintf(stderr, "[synth] gun preset=%d model=%d vol=%.2f dur=%.1f id=%lu\n", gun_preset, gun_force_model, volume, duration, (unsigned long)id); ac_log("[synth] gun preset=%d model=%d vol=%.2f dur=%.1f id=%lu\n", gun_preset, gun_force_model, volume, duration, (unsigned long)id); } else { id = audio_synth(audio, wt, freq, duration, volume, attack, decay, pan); fprintf(stderr, "[synth] type=%d freq=%.1f vol=%.2f dur=%.1f id=%lu\n", wt, freq, volume, duration, (unsigned long)id); ac_log("[synth] type=%d freq=%.1f vol=%.2f dur=%.1f id=%lu\n", wt, freq, volume, duration, (unsigned long)id); }
// For gun voices, an optional `params` object on opts passes per-shot // overrides into the C-side synth state (drag-to-edit on inspector // cards). Each numeric property maps to a gun_presets[] field key. // Applied immediately after synth init so the next audio thread tick // sees the patched values. if (is_gun && id) { JSValue params_v = JS_GetPropertyStr(ctx, opts, "params"); if (JS_IsObject(params_v)) { JSPropertyEnum *props = NULL; uint32_t plen = 0; if (JS_GetOwnPropertyNames(ctx, &props, &plen, params_v, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { for (uint32_t i = 0; i < plen; i++) { const char *k = JS_AtomToCString(ctx, props[i].atom); if (k) { JSValue pv = JS_GetProperty(ctx, params_v, props[i].atom); double pd; if (JS_IsNumber(pv) && JS_ToFloat64(ctx, &pd, pv) == 0) { audio_gun_voice_set_param(audio, id, k, pd); } JS_FreeValue(ctx, pv); JS_FreeCString(ctx, k); } JS_FreeAtom(ctx, props[i].atom); } js_free(ctx, props); } } JS_FreeValue(ctx, params_v); }
// Return sound object with kill(), update(), startedAt JSValue snd = JS_NewObject(ctx); JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id)); JS_SetPropertyStr(ctx, snd, "startedAt", JS_NewFloat64(ctx, audio->time));
// kill(fade) and update({volume,tone,pan}) methods JS_SetPropertyStr(ctx, snd, "kill", JS_NewCFunction(ctx, js_synth_obj_kill, "kill", 1)); JS_SetPropertyStr(ctx, snd, "update", JS_NewCFunction(ctx, js_synth_obj_update, "update", 1));
return snd;}
// sound.kill(idOrObj, fade) — accepts raw id or synth/sample/replay objectstatic JSValue js_sound_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio || argc < 1) return JS_UNDEFINED;
double id_d = 0; // Accept either a number (raw id) or an object with .id property if (JS_IsNumber(argv[0])) { JS_ToFloat64(ctx, &id_d, argv[0]); } else if (JS_IsObject(argv[0])) { JSValue id_v = JS_GetPropertyStr(ctx, argv[0], "id"); if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); JS_FreeValue(ctx, id_v); } uint64_t id = (uint64_t)id_d;
double fade = 0.025; if (argc >= 2 && JS_IsNumber(argv[1])) { JS_ToFloat64(ctx, &fade, argv[1]); }
// Check if it's a sample voice int is_sample = 0; int is_replay = 0; int is_zoo = 0; if (JS_IsObject(argv[0])) { JSValue is_v = JS_GetPropertyStr(ctx, argv[0], "isSample"); is_sample = JS_ToBool(ctx, is_v); JS_FreeValue(ctx, is_v); is_v = JS_GetPropertyStr(ctx, argv[0], "isReplay"); is_replay = JS_ToBool(ctx, is_v); JS_FreeValue(ctx, is_v); is_v = JS_GetPropertyStr(ctx, argv[0], "isZoo"); is_zoo = JS_ToBool(ctx, is_v); JS_FreeValue(ctx, is_v); }
if (is_replay) { audio_replay_kill(audio, id, fade); } else if (is_sample) { audio_sample_kill(audio, id, fade); } else if (is_zoo) { audio_oneshot_kill(audio, id, fade); } else { audio_kill(audio, id, fade); } return JS_UNDEFINED;}
// sound.update(id, {tone, volume, pan})static JSValue js_sound_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio || argc < 2) return JS_UNDEFINED;
double id_d; JS_ToFloat64(ctx, &id_d, argv[0]); uint64_t id = (uint64_t)id_d;
double freq = -1, vol = -1, pan = -3; if (JS_IsObject(argv[1])) { JSValue v; v = JS_GetPropertyStr(ctx, argv[1], "tone"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, argv[1], "volume"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, argv[1], "pan"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); JS_FreeValue(ctx, v); }
audio_update(audio, id, freq, vol, pan); return JS_UNDEFINED;}
// sound.freq(note) — convert note to Hzstatic JSValue js_sound_freq(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_NewFloat64(ctx, 440.0);
double f; if (JS_IsNumber(argv[0])) { JS_ToFloat64(ctx, &f, argv[0]); return JS_NewFloat64(ctx, f); }
const char *note = JS_ToCString(ctx, argv[0]); f = audio_note_to_freq(note); JS_FreeCString(ctx, note); return JS_NewFloat64(ctx, f);}
// sound.room.toggle()static JSValue js_room_toggle(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (current_rt->audio) audio_room_toggle(current_rt->audio); return JS_UNDEFINED;}
// sound.room.setMix(value)static JSValue js_set_room_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; double v; JS_ToFloat64(ctx, &v, argv[0]); audio_set_room_mix(current_rt->audio, (float)v); return JS_UNDEFINED;}
// sound.fx.setMix(value) — dry/wet for entire FX chainstatic JSValue js_set_fx_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; double v; JS_ToFloat64(ctx, &v, argv[0]); audio_set_fx_mix(current_rt->audio, (float)v); return JS_UNDEFINED;}
// sound.glitch.toggle()static JSValue js_glitch_toggle(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (current_rt->audio) audio_glitch_toggle(current_rt->audio); return JS_UNDEFINED;}
// sound.glitch.setMix(value)static JSValue js_set_glitch_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; double v; JS_ToFloat64(ctx, &v, argv[0]); audio_set_glitch_mix(current_rt->audio, (float)v); return JS_UNDEFINED;}
// sound.volume.setMix(value) — user-controlled master output gain (0..2)static JSValue js_set_master_volume(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; double v; JS_ToFloat64(ctx, &v, argv[0]); audio_set_master_volume(current_rt->audio, (float)v); return JS_UNDEFINED;}
// sound.volume.setMono(bool) / system.setMono(bool) — fold output to monostatic JSValue js_set_mono(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; audio_set_mono(current_rt->audio, JS_ToBool(ctx, argv[0])); return JS_UNDEFINED;}
// sound.drive.setMix(value) — tanh soft-clip dry/wet blend (0..1)static JSValue js_set_drive_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; double v; JS_ToFloat64(ctx, &v, argv[0]); audio_set_drive_mix(current_rt->audio, (float)v); return JS_UNDEFINED;}
// sound.speaker.setCapturePaused(bool) — pause/resume writes to the// output history ring. Used by notepat to freeze the buffer during// reverse-replay hold so the replay echo + overdubs don't contaminate// the captured wave.static JSValue js_speaker_set_capture_paused(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; int paused = JS_ToBool(ctx, argv[0]); audio_set_output_history_paused(current_rt->audio, paused); return JS_UNDEFINED;}
// sound.wobble.setMix(value) — flanger-ish dry/wet (0..1)static JSValue js_set_wobble_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; double v; JS_ToFloat64(ctx, &v, argv[0]); audio_set_wobble_mix(current_rt->audio, (float)v); return JS_UNDEFINED;}
// sound.microphone.open() — open device + start hot-mic threadstatic JSValue js_mic_open(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt->audio) return JS_FALSE; int ok = audio_mic_open(current_rt->audio); ac_log("[js][mic] open -> %s\n", ok == 0 ? "ok" : "fail"); return JS_NewBool(ctx, ok == 0);}
// sound.microphone.close() — stop hot-mic thread + close devicestatic JSValue js_mic_close(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt->audio) return JS_UNDEFINED; audio_mic_close(current_rt->audio); ac_log("[js][mic] close\n"); return JS_UNDEFINED;}
// sound.microphone.rec() — start buffering (instant, device already open)static JSValue js_mic_rec(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt->audio) return JS_FALSE; int ok = audio_mic_start(current_rt->audio); ac_log("[js][mic] rec -> %s\n", ok == 0 ? "ok" : "fail"); return JS_NewBool(ctx, ok == 0);}
// sound.microphone.cut() — stop buffering, return sample lengthstatic JSValue js_mic_cut(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt->audio) return JS_NewInt32(ctx, 0); int len = audio_mic_stop(current_rt->audio); ac_log("[js][mic] cut -> len=%d rate=%u err=%s\n", len, current_rt->audio->sample_rate, current_rt->audio->mic_last_error[0] ? current_rt->audio->mic_last_error : "(none)"); // Persist sample to disk for next boot if (len > 0) { int saved = audio_sample_save(current_rt->audio, "/mnt/ac-sample.raw"); ac_log("[js][mic] sample saved to /mnt/ac-sample.raw (%d samples)\n", saved); } return JS_NewInt32(ctx, len);}
// sound.microphone.recording — check if currently recordingstatic JSValue js_mic_recording(JSContext *ctx, JSValueConst this_val) { if (!current_rt->audio) return JS_FALSE; return JS_NewBool(ctx, current_rt->audio->recording);}
// sound.microphone.sampleLength — length of recorded samplestatic JSValue js_mic_sample_length(JSContext *ctx, JSValueConst this_val) { if (!current_rt->audio) return JS_NewInt32(ctx, 0); return JS_NewInt32(ctx, current_rt->audio->sample_len);}
// sound.microphone.sampleRate — capture rate of recorded samplestatic JSValue js_mic_sample_rate(JSContext *ctx, JSValueConst this_val) { if (!current_rt->audio) return JS_NewInt32(ctx, 0); return JS_NewInt32(ctx, current_rt->audio->sample_rate);}
// sampleObj.update({tone, base, volume, pan}) — update a playing sample voicestatic JSValue js_sample_obj_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ACAudio *audio = current_rt->audio; if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED;
JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); double id_d = 0; if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); JS_FreeValue(ctx, id_v); uint64_t id = (uint64_t)id_d; if (id == 0) return JS_UNDEFINED;
double freq = -1, base_freq = -1, vol = -1, pan = -3; JSValue v; v = JS_GetPropertyStr(ctx, argv[0], "tone"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[0], "base"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[0], "volume"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[0], "pan"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); JS_FreeValue(ctx, v);
if (freq > 0 && base_freq < 0) base_freq = 261.63; // default C4 audio_sample_update(audio, id, freq, base_freq, vol, pan); return JS_UNDEFINED;}
// replayObj.update({tone, base, volume, pan}) — update the dedicated replay voicestatic JSValue js_replay_obj_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ACAudio *audio = current_rt->audio; if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED;
JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); double id_d = 0; if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); JS_FreeValue(ctx, id_v); uint64_t id = (uint64_t)id_d; if (id == 0) return JS_UNDEFINED;
double freq = -1, base_freq = -1, vol = -1, pan = -3; JSValue v; v = JS_GetPropertyStr(ctx, argv[0], "tone"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[0], "base"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[0], "volume"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[0], "pan"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); JS_FreeValue(ctx, v);
if (freq > 0 && base_freq < 0) base_freq = 261.63; audio_replay_update(audio, id, freq, base_freq, vol, pan); return JS_UNDEFINED;}
// sound.sample.play({tone, base, volume, pan}) — play recorded sample at pitchstatic JSValue js_sample_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED;
JSValue opts = argv[0]; double freq = 261.63, base_freq = 261.63, volume = 0.7, pan = 0.0; JSValue v;
v = JS_GetPropertyStr(ctx, opts, "tone"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "base"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "volume"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &volume, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "pan"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); JS_FreeValue(ctx, v);
int loop = 0; v = JS_GetPropertyStr(ctx, opts, "loop"); if (JS_IsBool(v)) loop = JS_ToBool(ctx, v); JS_FreeValue(ctx, v);
uint64_t id = audio_sample_play(audio, freq, base_freq, volume, pan, loop); if (id == 0) { ac_log("[sample] play FAILED (sample_len=%d)\n", audio->sample_len); return JS_UNDEFINED; } ac_log("[sample] play OK id=%llu freq=%.1f vol=%.2f\n", (unsigned long long)id, freq, volume);
// Return object with id, update(), isSample JSValue snd = JS_NewObject(ctx); JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id)); JS_SetPropertyStr(ctx, snd, "isSample", JS_TRUE); JS_SetPropertyStr(ctx, snd, "update", JS_NewCFunction(ctx, js_sample_obj_update, "update", 1)); return snd;}
// sound.replay.play({tone, base, volume, pan}) — play dedicated global replay bufferstatic JSValue js_replay_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED;
JSValue opts = argv[0]; double freq = 261.63, base_freq = 261.63, volume = 0.7, pan = 0.0; JSValue v;
v = JS_GetPropertyStr(ctx, opts, "tone"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "base"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "volume"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &volume, v); JS_FreeValue(ctx, v);
v = JS_GetPropertyStr(ctx, opts, "pan"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); JS_FreeValue(ctx, v);
int loop = 0; v = JS_GetPropertyStr(ctx, opts, "loop"); if (JS_IsBool(v)) loop = JS_ToBool(ctx, v); JS_FreeValue(ctx, v);
uint64_t id = audio_replay_play(audio, freq, base_freq, volume, pan, loop); if (id == 0) return JS_UNDEFINED;
JSValue snd = JS_NewObject(ctx); JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id)); JS_SetPropertyStr(ctx, snd, "isReplay", JS_TRUE); JS_SetPropertyStr(ctx, snd, "update", JS_NewCFunction(ctx, js_replay_obj_update, "update", 1)); return snd;}
// sound.sample.kill(idOrObj, fade)static JSValue js_sample_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio || argc < 1) return JS_UNDEFINED;
double id_d = 0; if (JS_IsNumber(argv[0])) { JS_ToFloat64(ctx, &id_d, argv[0]); } else if (JS_IsObject(argv[0])) { JSValue id_v = JS_GetPropertyStr(ctx, argv[0], "id"); if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); JS_FreeValue(ctx, id_v); }
double fade = 0.02; if (argc >= 2 && JS_IsNumber(argv[1])) JS_ToFloat64(ctx, &fade, argv[1]);
audio_sample_kill(audio, (uint64_t)id_d, fade); return JS_UNDEFINED;}
// sound.zoo.play(name, { volume, pan, pitch }) — fire a named one-shot// sample from the zoo/lasers bank (see audio.c: audio_oneshot_play).// Returns an object { id, isZoo: true } so sound.kill() can route it// to audio_oneshot_kill the same way isSample / isReplay are handled.static JSValue js_zoo_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->audio || argc < 1) return JS_UNDEFINED; const char *name = JS_ToCString(ctx, argv[0]); if (!name) return JS_UNDEFINED;
double volume = 1.0, pan = 0.0, pitch = 1.0; if (argc > 1 && JS_IsObject(argv[1])) { JSValue v; v = JS_GetPropertyStr(ctx, argv[1], "volume"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &volume, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[1], "pan"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); JS_FreeValue(ctx, v); v = JS_GetPropertyStr(ctx, argv[1], "pitch"); if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pitch, v); JS_FreeValue(ctx, v); }
uint64_t id = audio_oneshot_play(current_rt->audio, name, volume, pan, pitch); JS_FreeCString(ctx, name); if (id == 0) return JS_UNDEFINED;
JSValue snd = JS_NewObject(ctx); JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id)); JS_SetPropertyStr(ctx, snd, "isZoo", JS_TRUE); return snd;}
// sound.zoo.kill(idOrObj, fade)static JSValue js_zoo_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->audio || argc < 1) return JS_UNDEFINED; double id_d = 0; if (JS_IsNumber(argv[0])) { JS_ToFloat64(ctx, &id_d, argv[0]); } else if (JS_IsObject(argv[0])) { JSValue id_v = JS_GetPropertyStr(ctx, argv[0], "id"); if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); JS_FreeValue(ctx, id_v); } double fade = 0.1; if (argc >= 2 && JS_IsNumber(argv[1])) JS_ToFloat64(ctx, &fade, argv[1]); audio_oneshot_kill(current_rt->audio, (uint64_t)id_d, fade); return JS_UNDEFINED;}
// sound.replay.kill(idOrObj, fade)static JSValue js_replay_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio || argc < 1) return JS_UNDEFINED;
double id_d = 0; if (JS_IsNumber(argv[0])) { JS_ToFloat64(ctx, &id_d, argv[0]); } else if (JS_IsObject(argv[0])) { JSValue id_v = JS_GetPropertyStr(ctx, argv[0], "id"); if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); JS_FreeValue(ctx, id_v); }
double fade = 0.02; if (argc >= 2 && JS_IsNumber(argv[1])) JS_ToFloat64(ctx, &fade, argv[1]);
audio_replay_kill(audio, (uint64_t)id_d, fade); return JS_UNDEFINED;}
// sound.sample.getData() — returns Float32Array of current sample buffer// Proper free callback for JS_NewArrayBuffer (3-arg signature, not plain free)static void js_free_array_buffer(JSRuntime *rt, void *opaque, void *ptr) { (void)rt; (void)opaque; free(ptr);}
static JSValue js_sample_get_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; ACAudio *audio = current_rt ? current_rt->audio : NULL; if (!audio || !audio->sample_buf || audio->sample_len <= 0) return JS_UNDEFINED;
// Lock to prevent sample_buf pointer swap during copy pthread_mutex_lock(&audio->lock); int len = audio->sample_len; if (len > audio->sample_max_len) len = audio->sample_max_len; if (len <= 0) { pthread_mutex_unlock(&audio->lock); return JS_UNDEFINED; }
size_t byte_len = (size_t)len * sizeof(float); float *copy = malloc(byte_len); if (!copy) { pthread_mutex_unlock(&audio->lock); return JS_UNDEFINED; } memcpy(copy, audio->sample_buf, byte_len); pthread_mutex_unlock(&audio->lock);
// Create ArrayBuffer from our copy JSValue ab = JS_NewArrayBuffer(ctx, (uint8_t *)copy, byte_len, js_free_array_buffer, NULL, 0); if (JS_IsException(ab)) { free(copy); return JS_UNDEFINED; }
// Create Float32Array view JSValue global = JS_GetGlobalObject(ctx); JSValue ctor = JS_GetPropertyStr(ctx, global, "Float32Array"); JSValue f32 = JS_CallConstructor(ctx, ctor, 1, &ab); JS_FreeValue(ctx, ctor); JS_FreeValue(ctx, global); JS_FreeValue(ctx, ab); return f32;}
// sound.sample.saveTo(path) — save current sample to a file, returns sample count or -1static JSValue js_sample_save_to(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->audio || argc < 1) return JS_NewInt32(ctx, -1); const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_NewInt32(ctx, -1); int result = audio_sample_save(current_rt->audio, path); ac_log("[sample] saveTo(%s) -> %d samples\n", path, result); JS_FreeCString(ctx, path); return JS_NewInt32(ctx, result);}
// sound.sample.loadFrom(path) — load sample from a file, returns sample count or -1static JSValue js_sample_load_from(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->audio || argc < 1) return JS_NewInt32(ctx, -1); const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_NewInt32(ctx, -1); int result = audio_sample_load(current_rt->audio, path); ac_log("[sample] loadFrom(%s) -> %d samples\n", path, result); JS_FreeCString(ctx, path); return JS_NewInt32(ctx, result);}
// sound.sample.loadData(float32array, rate) — load sample data from JS arraystatic JSValue js_sample_load_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt) return JS_FALSE; ACAudio *audio = current_rt->audio; if (!audio || argc < 1) return JS_FALSE;
size_t byte_len = 0; size_t byte_off = 0; size_t bytes_per = 0; JSValue ab = JS_GetTypedArrayBuffer(ctx, argv[0], &byte_off, &byte_len, &bytes_per); if (JS_IsException(ab)) return JS_FALSE;
size_t ab_len = 0; uint8_t *ptr = JS_GetArrayBuffer(ctx, &ab_len, ab); if (!ptr) { JS_FreeValue(ctx, ab); return JS_FALSE; }
float *data = (float *)(ptr + byte_off); int len = (int)(byte_len / sizeof(float)); ac_log("[sample] loadData: byte_len=%zu byte_off=%zu bytes_per=%zu ab_len=%zu len=%d\n", byte_len, byte_off, bytes_per, ab_len, len);
unsigned int rate = 48000; if (argc >= 2 && JS_IsNumber(argv[1])) { double r; JS_ToFloat64(ctx, &r, argv[1]); if (r > 0) rate = (unsigned int)r; }
audio_sample_load_data(audio, data, len, rate); JS_FreeValue(ctx, ab); // Free AFTER memcpy — ptr is into this buffer return JS_TRUE;}
// sound.replay.loadData(float32array, rate) — load data into dedicated replay bufferstatic JSValue js_replay_load_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt) return JS_FALSE; ACAudio *audio = current_rt->audio; if (!audio || argc < 1) return JS_FALSE;
size_t byte_len = 0; size_t byte_off = 0; size_t bytes_per = 0; JSValue ab = JS_GetTypedArrayBuffer(ctx, argv[0], &byte_off, &byte_len, &bytes_per); if (JS_IsException(ab)) return JS_FALSE;
size_t ab_len = 0; uint8_t *ptr = JS_GetArrayBuffer(ctx, &ab_len, ab); if (!ptr) { JS_FreeValue(ctx, ab); return JS_FALSE; }
float *data = (float *)(ptr + byte_off); int len = (int)(byte_len / sizeof(float));
unsigned int rate = 48000; if (argc >= 2 && JS_IsNumber(argv[1])) { double r; JS_ToFloat64(ctx, &r, argv[1]); if (r > 0) rate = (unsigned int)r; }
audio_replay_load_data(audio, data, len, rate); JS_FreeValue(ctx, ab); return JS_TRUE;}
// sound.speaker.drawStrip(x, y, w, h, seconds, needleFrac, viewOffsetSec)// Renders a scrolling waveform strip in one C call. The needle stays at// `needleFrac` (0.0..1.0) of the strip width and represents the current// PLAYBACK CURSOR — what's being heard right now. The wave never jumps:// it continuously drifts based on how the cursor moves through the buffer// across frames.//// viewOffsetSec controls the cursor's position relative to "now"://// 0.0 → cursor sits at the latest captured sample. Past audio// extends to the LEFT of the needle. Right of needle is empty// (no future). As real time advances, freshly-captured samples// appear at the needle and the wave drifts LEFT — the natural// scroll for live capture.//// > 0 → cursor is OFFSET-seconds in the past. Left of needle still// shows the LEFT-PAST (older than cursor). Right of needle// shows the RIGHT-PAST (samples captured AFTER the cursor// position, up to the live edge). As JS grows the offset (e.g.// while spacebar is held), the cursor retreats further into// the past and the wave appears to drift RIGHT — exactly// matching a backwards-replay scrub.//// shrinking offset on release → cursor catches back up to "now" and// the wave drifts LEFT (forward) until the right side empties// and we're back in live capture mode.//// Per-pixel peak math is C-native (no JS loop overhead); the audio ring// slice is copied under audio->lock then drawn unlocked.static JSValue js_speaker_draw_strip(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 4) return JS_UNDEFINED; ACAudio *audio = current_rt->audio; ACGraph *g = current_rt->graph; if (!audio || !audio->output_history_buf || audio->output_history_size <= 0 || !g) { return JS_UNDEFINED; }
int x = 0, y = 0, w = 0, h = 0; JS_ToInt32(ctx, &x, argv[0]); JS_ToInt32(ctx, &y, argv[1]); JS_ToInt32(ctx, &w, argv[2]); JS_ToInt32(ctx, &h, argv[3]); double seconds = 4.0; if (argc >= 5 && JS_IsNumber(argv[4])) JS_ToFloat64(ctx, &seconds, argv[4]); double needle_frac = 0.5; if (argc >= 6 && JS_IsNumber(argv[5])) JS_ToFloat64(ctx, &needle_frac, argv[5]); double view_offset_sec = 0.0; if (argc >= 7 && JS_IsNumber(argv[6])) JS_ToFloat64(ctx, &view_offset_sec, argv[6]); if (view_offset_sec < 0.0) view_offset_sec = 0.0;
if (w <= 2 || h <= 2 || seconds <= 0.0) return JS_UNDEFINED; if (needle_frac < 0.0) needle_frac = 0.0; if (needle_frac > 1.0) needle_frac = 1.0;
int needle_off = (int)((double)w * needle_frac + 0.5); if (needle_off < 0) needle_off = 0; if (needle_off >= w) needle_off = w - 1; int needle_x = x + needle_off;
// Strip draws past `seconds` of audio ending AT the cursor, BUT only // in the LEFT half (pixels 0 .. needle_off). This way the newest // sample sits immediately left of the needle — the wave visibly // "emerges" from the playhead and flows leftward into the past. // Right of the needle stays empty (no double-layer from reverse- // replay echo; no confusion about where new audio comes from). int left_w = needle_off > 0 ? needle_off : w; double left_seconds = seconds;
pthread_mutex_lock(&audio->lock); unsigned int rate = audio->output_history_rate ? audio->output_history_rate : AUDIO_OUTPUT_HISTORY_RATE; int hist_size = audio->output_history_size; uint64_t write_pos = audio->output_history_write_pos; int available = write_pos < (uint64_t)hist_size ? (int)write_pos : hist_size;
// cursor_pos: ring index of "where the playhead sits" in samples. // = write_pos - offset_samples uint64_t offset_samples = (uint64_t)(view_offset_sec * (double)rate + 0.5); if (offset_samples > write_pos) offset_samples = write_pos; uint64_t cursor_pos = write_pos - offset_samples;
// Snapshot the past slice [cursor - left_seconds, cursor]. This is // the ONLY slice we render — no right-of-cursor sampling to avoid // the double-layer effect from reverse-replay feeding back into // the speaker output ring during hold. int left_samples_want = (int)(left_seconds * (double)rate + 0.5); if (left_samples_want < 1) left_samples_want = 1; if ((uint64_t)left_samples_want > cursor_pos) left_samples_want = (int)cursor_pos; if (left_samples_want > available) left_samples_want = available;
float *left_copy = NULL; if (left_samples_want > 0) { left_copy = (float *)malloc((size_t)left_samples_want * sizeof(float)); if (left_copy) { uint64_t start = cursor_pos - (uint64_t)left_samples_want; for (int i = 0; i < left_samples_want; i++) { left_copy[i] = audio->output_history_buf[(start + (uint64_t)i) % (uint64_t)hist_size]; } } } pthread_mutex_unlock(&audio->lock);
int midY = y + h / 2; int amp = (int)((double)h * 0.45); if (amp < 1) amp = 1; ACColor saved = g->ink;
// Background + zero-line graph_ink(g, (ACColor){20, 15, 30, 160}); graph_box(g, x, y, w, h, 1); graph_ink(g, (ACColor){80, 80, 90, 120}); graph_line(g, x, midY, x + w - 1, midY);
// Inner draw helper: render `len` samples across `pixel_w` pixels // starting at draw_x, with column 0 = oldest sample. #define DRAW_SLICE(buf, len, pixel_w, draw_x_off) do { \ if ((buf) && (len) > 0 && (pixel_w) > 0) { \ double spc = (double)(len) / (double)(pixel_w); \ for (int col = 0; col < (pixel_w); col++) { \ int i0 = (int)((double)col * spc); \ int i1 = (int)((double)(col + 1) * spc); \ if (i1 > (len)) i1 = (len); \ if (i0 < 0) i0 = 0; \ if (i1 <= i0) continue; \ float peak = 0.0f; \ for (int i = i0; i < i1; i++) { \ float a = (buf)[i]; \ if (a < 0) a = -a; \ if (a > peak) peak = a; \ } \ if (peak > 1.0f) peak = 1.0f; \ int bar_h = (int)(peak * (float)amp + 0.5f); \ if (bar_h < 1) bar_h = 1; \ /* Cool → bright palette: deep teal at low peaks, cyan- \ * white at high peaks. Reads clearly against the strip's \ * dark-purple background and keeps the warm red/green \ * needles fully legible on top. */ \ int r = 40 + (int)(peak * 180.0f + 0.5f); if (r > 255) r = 255; \ int gc = 160 + (int)(peak * 90.0f + 0.5f); if (gc > 255) gc = 255; \ int b_ = 200 + (int)(peak * 55.0f + 0.5f); if (b_ > 255) b_ = 255; \ graph_ink(g, (ACColor){(uint8_t)r, (uint8_t)gc, (uint8_t)b_, 220}); \ int dx = x + (draw_x_off) + col; \ graph_line(g, dx, midY - bar_h, dx, midY + bar_h); \ } \ } \ } while (0)
// Single slice rendered across full strip width — newest sample at // the rightmost pixel, oldest at column 0. Needle at needle_frac is // drawn on top as a visual marker of "playhead position". DRAW_SLICE(left_copy, left_samples_want, left_w, 0);
#undef DRAW_SLICE
if (left_copy) free(left_copy);
// Playhead needle — draw last so it sits on top of the bars. Color // tints toward orange when the cursor is offset (replay mode) so it's // visually distinct from the live red needle. if (view_offset_sec > 0.001) { graph_ink(g, (ACColor){255, 160, 60, 230}); } else { graph_ink(g, (ACColor){240, 80, 80, 220}); } graph_line(g, needle_x, y, needle_x, y + h - 1);
g->ink = saved; return JS_UNDEFINED;}
// sound.speaker.getRecentBuffer(seconds) -> { data: Float32Array, rate: number }static JSValue js_speaker_get_recent_buffer(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt ? current_rt->audio : NULL; if (!audio || !audio->output_history_buf || audio->output_history_size <= 0) return JS_NULL;
double seconds = 1.0; if (argc >= 1 && JS_IsNumber(argv[0])) JS_ToFloat64(ctx, &seconds, argv[0]); if (seconds <= 0.0) return JS_NULL;
unsigned int rate_guess = audio->output_history_rate ? audio->output_history_rate : AUDIO_OUTPUT_HISTORY_RATE; int want_len = (int)(seconds * (double)rate_guess + 0.5); if (want_len < 1) want_len = 1; if (want_len > audio->output_history_size) want_len = audio->output_history_size;
float *copy = malloc((size_t)want_len * sizeof(float)); if (!copy) return JS_NULL;
unsigned int actual_rate = 0; int len = audio_output_get_recent(audio, copy, want_len, &actual_rate); if (len <= 0 || actual_rate == 0) { free(copy); return JS_NULL; }
size_t byte_len = (size_t)len * sizeof(float); JSValue ab = JS_NewArrayBuffer(ctx, (uint8_t *)copy, byte_len, js_free_array_buffer, NULL, 0); if (JS_IsException(ab)) { free(copy); return JS_NULL; }
JSValue global = JS_GetGlobalObject(ctx); JSValue ctor = JS_GetPropertyStr(ctx, global, "Float32Array"); JSValue f32 = JS_CallConstructor(ctx, ctor, 1, &ab); JS_FreeValue(ctx, ctor); JS_FreeValue(ctx, global); JS_FreeValue(ctx, ab); if (JS_IsException(f32)) return JS_NULL;
JSValue out = JS_NewObject(ctx); JS_SetPropertyStr(ctx, out, "data", f32); JS_SetPropertyStr(ctx, out, "rate", JS_NewInt32(ctx, (int)actual_rate)); return out;}
// sound.tape.recording() -> bool// Returns true while the tape recorder (recorder.c) is capturing.// Pure read-only state getter for notepat UI.static JSValue js_tape_recording(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv;#ifdef HAVE_AVCODEC if (current_rt && current_rt->recorder) { return JS_NewBool(ctx, recorder_is_recording((ACRecorder *)current_rt->recorder)); }#endif return JS_NewBool(ctx, 0);}
// sound.tape.elapsed() -> number (seconds, 0 if not recording)static JSValue js_tape_elapsed(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv;#ifdef HAVE_AVCODEC if (current_rt && current_rt->recorder) { return JS_NewFloat64(ctx, recorder_elapsed((ACRecorder *)current_rt->recorder)); }#endif return JS_NewFloat64(ctx, 0.0);}
// sound.speak(text)static JSValue js_speak(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->tts) return JS_UNDEFINED; const char *text = JS_ToCString(ctx, argv[0]); if (!text) return JS_UNDEFINED; tts_speak(current_rt->tts, text); JS_FreeCString(ctx, text); return JS_UNDEFINED;}
// sound.speakVoice(text, male) — speak with voice selection (0=female, 1=male)static JSValue js_speak_voice(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2 || !current_rt->tts) return JS_UNDEFINED; const char *text = JS_ToCString(ctx, argv[0]); if (!text) return JS_UNDEFINED; int male = 0; JS_ToInt32(ctx, &male, argv[1]); tts_speak_voice(current_rt->tts, text, male); JS_FreeCString(ctx, text); return JS_UNDEFINED;}
// sound.speakCached(key) — instant playback of pre-rendered letter/keystatic JSValue js_speak_cached(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt->tts) return JS_UNDEFINED; const char *key = JS_ToCString(ctx, argv[0]); if (!key) return JS_UNDEFINED; tts_speak_cached(current_rt->tts, key); JS_FreeCString(ctx, key); return JS_UNDEFINED;}
// ============================================================// Event System// ============================================================
static JSValue js_event_is(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { if (argc < 1) return JS_FALSE; const char *pattern = JS_ToCString(ctx, argv[0]); if (!pattern) return JS_FALSE;
JSValue type_val = JS_GetPropertyStr(ctx, this_val, "_type"); JSValue key_val = JS_GetPropertyStr(ctx, this_val, "_key");
int type; JS_ToInt32(ctx, &type, type_val); const char *key = JS_ToCString(ctx, key_val);
int match = 0;
if (strcmp(pattern, "touch") == 0) match = (type == AC_EVENT_TOUCH); else if (strcmp(pattern, "lift") == 0) match = (type == AC_EVENT_LIFT); else if (strcmp(pattern, "draw") == 0) match = (type == AC_EVENT_DRAW); else if (strcmp(pattern, "keyboard:down") == 0) match = (type == AC_EVENT_KEYBOARD_DOWN); else if (strcmp(pattern, "keyboard:up") == 0) match = (type == AC_EVENT_KEYBOARD_UP); else if (strncmp(pattern, "keyboard:down:", 14) == 0) match = (type == AC_EVENT_KEYBOARD_DOWN && key && strcmp(key, pattern + 14) == 0); else if (strncmp(pattern, "keyboard:up:", 12) == 0) match = (type == AC_EVENT_KEYBOARD_UP && key && strcmp(key, pattern + 12) == 0); else if (strcmp(pattern, "reframed") == 0) match = 0; // no resize on bare metal else if (strncmp(pattern, "gamepad", 7) == 0) match = 0; // stub else if (strncmp(pattern, "midi:", 5) == 0) match = 0; // stub else if (strcmp(pattern, "compose") == 0) match = 0; // stub
JS_FreeCString(ctx, pattern); JS_FreeCString(ctx, key); JS_FreeValue(ctx, type_val); JS_FreeValue(ctx, key_val);
return JS_NewBool(ctx, match);}
static JSValue make_event_object(JSContext *ctx, ACEvent *ev) { JSValue obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, obj, "_type", JS_NewInt32(ctx, ev->type)); JS_SetPropertyStr(ctx, obj, "_key", JS_NewString(ctx, ev->key_name)); JS_SetPropertyStr(ctx, obj, "x", JS_NewInt32(ctx, ev->x)); JS_SetPropertyStr(ctx, obj, "y", JS_NewInt32(ctx, ev->y)); JS_SetPropertyStr(ctx, obj, "repeat", JS_NewBool(ctx, ev->repeat)); JS_SetPropertyStr(ctx, obj, "key", JS_NewString(ctx, ev->key_name)); JS_SetPropertyStr(ctx, obj, "code", JS_NewString(ctx, ev->key_name)); // Velocity: 0-127 from analog pressure (0.0-1.0), default 127 for digital keys int velocity = ev->pressure > 0.001f ? (int)(ev->pressure * 127.0f) : 127; if (velocity < 1 && ev->type == AC_EVENT_KEYBOARD_DOWN) velocity = 1; JS_SetPropertyStr(ctx, obj, "velocity", JS_NewInt32(ctx, velocity)); JS_SetPropertyStr(ctx, obj, "pressure", JS_NewFloat64(ctx, (double)ev->pressure));
// Web parity: event.name is the full event string ("keyboard:down:space", // "touch", "lift", …) — pieces test it with name.startsWith("keyboard:down") // (the key alone lives in event.key / event.code above). { char full_name[96]; switch (ev->type) { case AC_EVENT_KEYBOARD_DOWN: snprintf(full_name, sizeof(full_name), "keyboard:down:%s", ev->key_name); break; case AC_EVENT_KEYBOARD_UP: snprintf(full_name, sizeof(full_name), "keyboard:up:%s", ev->key_name); break; case AC_EVENT_TOUCH: snprintf(full_name, sizeof(full_name), "touch"); break; case AC_EVENT_DRAW: snprintf(full_name, sizeof(full_name), "draw"); break; case AC_EVENT_LIFT: snprintf(full_name, sizeof(full_name), "lift"); break; case AC_EVENT_REFRAMED: snprintf(full_name, sizeof(full_name), "reframed"); break; default: snprintf(full_name, sizeof(full_name), "%s", ev->key_name); break; } JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, full_name)); }
// pointer sub-object JSValue pointer = JS_NewObject(ctx); JS_SetPropertyStr(ctx, pointer, "x", JS_NewInt32(ctx, ev->x)); JS_SetPropertyStr(ctx, pointer, "y", JS_NewInt32(ctx, ev->y)); JS_SetPropertyStr(ctx, obj, "pointer", pointer);
JS_SetPropertyStr(ctx, obj, "is", JS_NewCFunction(ctx, js_event_is, "is", 1));
return obj;}
// ============================================================// num utilities// ============================================================
static JSValue js_num_clamp(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 3) return JS_UNDEFINED; double v, mn, mx; JS_ToFloat64(ctx, &v, argv[0]); JS_ToFloat64(ctx, &mn, argv[1]); JS_ToFloat64(ctx, &mx, argv[2]); if (v < mn) v = mn; if (v > mx) v = mx; return JS_NewFloat64(ctx, v);}
static JSValue js_num_rand(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; return JS_NewFloat64(ctx, (double)rand() / RAND_MAX);}
static JSValue js_num_randint(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2) return JS_NewInt32(ctx, 0); int a, b; JS_ToInt32(ctx, &a, argv[0]); JS_ToInt32(ctx, &b, argv[1]); if (b <= a) return JS_NewInt32(ctx, a); return JS_NewInt32(ctx, a + rand() % (b - a + 1));}
// num.randInt(n) — random integer 0..n INCLUSIVE (web-parity; nom and other// web pieces rely on the inclusive upper bound).static JSValue js_num_randint_single(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; int n = 0; if (argc >= 1) JS_ToInt32(ctx, &n, argv[0]); if (n <= 0) return JS_NewInt32(ctx, 0); return JS_NewInt32(ctx, rand() % (n + 1));}
// seconds(n) — convert wall seconds to sim ticks. Native calls sim() once// per ~60Hz display frame; pieces feed this straight into gizmo.Hourglass.static JSValue js_seconds(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; double s = 0; if (argc >= 1) JS_ToFloat64(ctx, &s, argv[0]); return JS_NewFloat64(ctx, s * 60.0);}
// ============================================================// Module loader for ES module imports// ============================================================
static JSModuleDef *js_module_loader(JSContext *ctx, const char *module_name, void *opaque) { (void)opaque;
// Check if it's a stub module const char *stub_src = NULL;
if (strstr(module_name, "chord-detection")) { stub_src = "export function detectChord() { return null; }"; } else if (strstr(module_name, "gamepad-diagram")) { stub_src = "export function drawMiniControllerDiagram() {}"; } else if (strstr(module_name, "pixel-sample")) { stub_src = "export function decodeBitmapToSample() { return null; }\n" "export function loadPaintingAsAudio() { return null; }"; } else if (strstr(module_name, "nopaint")) { stub_src = "export function nopaint_generateColoredLabel() {}\n" "export function nopaint_boot() {}\n" "export function nopaint_act() {}\n" "export function nopaint_paint() {}\n" "export function nopaint_is(s) { return false; }\n" "export function nopaint_cancelStroke() {}\n" "export function nopaint_adjust() {}\n" "export function nopaint_handleColor(c, ink) { return ink(c); }\n" "export function nopaint_cleanupColor() {}\n" "export function nopaint_parseBrushParams(o) { return {color:[], mode:'fill', thickness:1}; }\n" "export function nopaint_renderPerfHUD() {}\n" "export function nopaint_triggerBakeFlash() {}"; } else if (strstr(module_name, "color-highlighting")) { stub_src = "export function generateNopaintHUDLabel() { return ''; }\n" "export function colorizeColorName() { return ''; }"; }
// Try to load the module from the filesystem char resolved[512] = {0};
if (!stub_src) { // Try multiple paths const char *search_paths[] = { module_name, NULL };
// If it's a relative path like "../lib/note-colors.mjs", resolve from / if (module_name[0] == '.' && module_name[1] == '.') { // "../lib/X" → "/lib/X" const char *p = module_name + 2; while (*p == '/') p++; if (*p == '.') { // "../../lib/X" p++; while (*p == '.') p++; while (*p == '/') p++; } snprintf(resolved, sizeof(resolved), "/%s", p); } else { snprintf(resolved, sizeof(resolved), "%s", module_name); }
FILE *f = fopen(resolved, "r"); if (!f) { // Try /lib/ prefix snprintf(resolved, sizeof(resolved), "/lib/%s", strrchr(module_name, '/') ? strrchr(module_name, '/') + 1 : module_name); f = fopen(resolved, "r"); } if (f) { fseek(f, 0, SEEK_END); long len = ftell(f); fseek(f, 0, SEEK_SET); char *src = malloc(len + 1); fread(src, 1, len, f); src[len] = '\0'; fclose(f);
JSValue val = JS_Eval(ctx, src, len, module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); free(src);
if (JS_IsException(val)) { JSValue exc = JS_GetException(ctx); const char *str = JS_ToCString(ctx, exc); fprintf(stderr, "[js] Module load error (%s): %s\n", module_name, str); JS_FreeCString(ctx, str); JS_FreeValue(ctx, exc); return NULL; }
JSModuleDef *m = (JSModuleDef *)JS_VALUE_GET_PTR(val); return m; }
// Module not found — create empty stub fprintf(stderr, "[js] Module not found, stubbing: %s\n", module_name); stub_src = "// stub"; }
// Compile stub module JSValue val = JS_Eval(ctx, stub_src, strlen(stub_src), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); if (JS_IsException(val)) { JSValue exc = JS_GetException(ctx); const char *str = JS_ToCString(ctx, exc); fprintf(stderr, "[js] Stub module error (%s): %s\n", module_name, str); JS_FreeCString(ctx, str); JS_FreeValue(ctx, exc); return NULL; }
JSModuleDef *m = (JSModuleDef *)JS_VALUE_GET_PTR(val); return m;}
// ============================================================// Runtime Init / Lifecycle// ============================================================
// JS init code evaluated before pieces — defines Button, Box, etc.static const char *js_init_code = "globalThis.__Button = class Button {\n" " constructor(x, y, w, h) {\n" " if (typeof x === 'object' && x !== null) {\n" " this.box = { x: x.x || 0, y: x.y || 0, w: x.w || x.width || 0, h: x.h || x.height || 0 };\n" " } else {\n" " this.box = { x: x || 0, y: y || 0, w: w || 0, h: h || 0 };\n" " }\n" " this.down = false;\n" " this.over = false;\n" " this.disabled = false;\n" " this.multitouch = true;\n" " }\n" " get up() { return !this.down; }\n" " set up(v) { this.down = !v; }\n" " paint(fn) { if (!this.disabled && fn) fn(this); }\n" " act(e, callbacks, pens) {\n" " if (this.disabled) return;\n" " if (typeof callbacks === 'function') callbacks = { push: callbacks };\n" " this.actions = callbacks;\n" " }\n" "};\n" "globalThis.__Box = class Box {\n" " constructor(x, y, w, h) {\n" " if (typeof x === 'object') { this.x = x.x; this.y = x.y; this.w = x.w; this.h = x.h; }\n" " else { this.x = x || 0; this.y = y || 0; this.w = w || 0; this.h = h || 0; }\n" " }\n" " static from(obj) { return new __Box(obj); }\n" "};\n" "globalThis.Number.isFinite = globalThis.Number.isFinite || function(v) { return typeof v === 'number' && isFinite(v); };\n" // num.shiftRGB — lerp between two RGB arrays "globalThis.__shiftRGB = function(from, to, t, mode) {\n" " if (!from || !to) return from || [0,0,0];\n" " t = Math.max(0, Math.min(1, t || 0));\n" " return [\n" " Math.round(from[0] + (to[0] - from[0]) * t),\n" " Math.round(from[1] + (to[1] - from[1]) * t),\n" " Math.round(from[2] + (to[2] - from[2]) * t)\n" " ];\n" "};\n" // num.dist — Euclidean distance "globalThis.__dist = function(x1, y1, x2, y2) {\n" " const dx = x2 - x1, dy = y2 - y1;\n" " return Math.sqrt(dx*dx + dy*dy);\n" "};\n" // num.map — map value from one range to another "globalThis.__map = function(v, inMin, inMax, outMin, outMax) {\n" " return outMin + (v - inMin) * (outMax - outMin) / (inMax - inMin);\n" "};\n" // num.lerp "globalThis.__lerp = function(a, b, t) { return a + (b - a) * t; };\n" // num.parseColor — parse color from params array (e.g. ["purple"], ["255","0","0"], ["128"]) "globalThis.__parseColor = function(params) {\n" " if (!params || params.length === 0) return [];\n" " var names = {red:[255,0,0],orange:[255,165,0],yellow:[255,255,0],green:[0,128,0],\n" " cyan:[0,255,255],blue:[0,0,255],purple:[128,0,128],magenta:[255,0,255],\n" " pink:[255,192,203],white:[255,255,255],gray:[128,128,128],grey:[128,128,128],\n" " black:[0,0,0],brown:[139,69,19]};\n" " var first = params[0];\n" " if (typeof first === 'string' && names[first.toLowerCase()]) {\n" " var c = names[first.toLowerCase()].slice();\n" " if (params.length >= 2) c.push(parseInt(params[1]) || 255);\n" " return c;\n" " }\n" " var nums = params.map(function(p){return parseInt(p)}).filter(function(n){return !isNaN(n)});\n" " return nums;\n" "};\n" // num.randIntArr — array of N random ints in [0, max] "globalThis.__randIntArr = function(max, len) {\n" " var a = []; for (var i = 0; i < len; i++) a.push(Math.floor(Math.random() * (max + 1)));\n" " return a;\n" "};\n" // num.timestamp — returns a timestamp string "globalThis.__timestamp = function() { return Date.now().toString(36); };\n" // setTimeout/clearTimeout — queued here, flushed once per sim tick from // js_call_sim via __flushTimeouts (QuickJS has no event loop of its own). // Browser-close-enough for the short audio/UI schedules pieces use. "globalThis.__timeoutQueue = [];\n" "globalThis.__timeoutId = 1;\n" "globalThis.setTimeout = globalThis.setTimeout || function(fn, delayMs) {\n" " if (typeof fn !== 'function') return 0;\n" " var id = globalThis.__timeoutId++;\n" " globalThis.__timeoutQueue.push({ id: id, fn: fn, at: Date.now() + (Number(delayMs) || 0) });\n" " return id;\n" "};\n" "globalThis.clearTimeout = globalThis.clearTimeout || function(id) {\n" " var q = globalThis.__timeoutQueue;\n" " for (var i = 0; i < q.length; i++) if (q[i].id === id) { q.splice(i, 1); return; }\n" "};\n" "globalThis.__flushTimeouts = function() {\n" " var q = globalThis.__timeoutQueue;\n" " if (q.length === 0) return;\n" " var now = Date.now();\n" " for (var i = 0; i < q.length; ) {\n" " if (now >= q[i].at) { var e = q.splice(i, 1)[0]; try { e.fn(); } catch (err) { console.error('setTimeout cb error:', err); } }\n" " else i++;\n" " }\n" "};\n" // gizmo.Hourglass — repeatable sim-tick timer (mirror of web lib/gizmo.mjs). // Pieces step it once per sim(); with autoFlip it fires `flipped` every // `max` ticks (nom's troggle clock and friends). "globalThis.__Hourglass = class Hourglass {\n" " constructor(max = 1, opts = {}, startingTicks = 0) {\n" " this.max = max; this.ticks = startingTicks; this.complete = false;\n" " this.flips = 0n; this.autoFlip = opts.autoFlip || false;\n" " this._completed = opts.completed; this._flipped = opts.flipped; this._every = opts.every;\n" " }\n" " step() {\n" " if (this.complete === true) return;\n" " this.ticks += 1;\n" " if (this._every) this._every();\n" " this.progress = this.ticks / this.max;\n" " if (this.ticks >= this.max) {\n" " this.complete = true; this.progress = 1;\n" " if (this._completed) this._completed(this.flips);\n" " if (this.autoFlip) this.flip();\n" " }\n" " }\n" " flip() {\n" " this.flips += 1n; this.ticks = 0; this.complete = false;\n" " if (this._flipped) this._flipped(this.flips, ...arguments);\n" " }\n" "};\n" // nopaint_generateColoredLabel stub (native doesn't have HUD color highlighting) "globalThis.__nopaint_generateColoredLabel = function() {};\n" // Typeface constructor stub "globalThis.__Typeface = function Typeface(name) {\n" " this.name = name;\n" " this.loaded = false;\n" " this.load = function(preloadFn) { this.loaded = true; return this; };\n" " this.measure = function(text) { return { width: (text||'').length * 8, height: 8 }; };\n" "};\n" // ── Global theme system ── // Auto-switches dark/light based on LA time. All pieces use theme.fg, theme.bg, etc. // Supports named presets via __theme.apply(id) that persist via config.json. "globalThis.__theme = (function() {\n" " function getLAOffset() {\n" " var d = new Date(), m = d.getUTCMonth(), y = d.getUTCFullYear();\n" " if (m > 2 && m < 10) return 7;\n" " if (m < 2 || m > 10) return 8;\n" " if (m === 2) {\n" " var mar1 = new Date(y, 2, 1), ss = 8 + (7 - mar1.getDay()) % 7;\n" " return d.getUTCDate() > ss || (d.getUTCDate() === ss && d.getUTCHours() >= 10) ? 7 : 8;\n" " }\n" " var nov1 = new Date(y, 10, 1), fs = 1 + (7 - nov1.getDay()) % 7;\n" " return d.getUTCDate() < fs || (d.getUTCDate() === fs && d.getUTCHours() < 9) ? 7 : 8;\n" " }\n" " function getLAHour() {\n" " return (new Date().getUTCHours() - getLAOffset() + 24) % 24;\n" " }\n" " var t = { dark: true, _lastCheck: 0, _overrideId: null, _override: null };\n" " // Theme presets: each has dark and light color overrides\n" " t.presets = {\n" " serious: {\n" " label: 'serious', desc: 'black & white',\n" " dark: { bg:[0,0,0], bgAlt:[10,10,10], bgDim:[0,0,0],\n" " fg:255, fgDim:160, fgMute:90,\n" " bar:[15,15,15], border:[60,60,60],\n" " accent:[128,128,128], ok:[200,200,200], err:[255,100,100],\n" " warn:[200,200,100], link:[180,180,255],\n" " pad:[10,10,10], padSharp:[5,5,5], padLine:[40,40,40],\n" " cursor:[255,255,255] },\n" " light: { bg:[255,255,255], bgAlt:[245,245,245], bgDim:[235,235,235],\n" " fg:0, fgDim:80, fgMute:160,\n" " bar:[240,240,240], border:[180,180,180],\n" " accent:[100,100,100], ok:[40,40,40], err:[180,40,40],\n" " warn:[120,100,20], link:[40,40,180],\n" " pad:[245,245,245], padSharp:[230,230,230], padLine:[200,200,200],\n" " cursor:[0,0,0] }\n" " },\n" " neo: {\n" " label: 'neo', desc: 'lime & black',\n" " dark: { bg:[0,0,0], bgAlt:[5,10,5], bgDim:[0,0,0],\n" " fg:200, fgDim:120, fgMute:60,\n" " bar:[5,15,5], border:[0,80,0],\n" " accent:[0,200,80], ok:[0,255,0], err:[255,50,50],\n" " warn:[200,255,0], link:[0,180,255],\n" " pad:[5,10,5], padSharp:[0,5,0], padLine:[0,50,0],\n" " cursor:[0,255,80] },\n" " light: { bg:[220,255,220], bgAlt:[230,255,230], bgDim:[200,240,200],\n" " fg:10, fgDim:60, fgMute:120,\n" " bar:[200,240,200], border:[100,180,100],\n" " accent:[0,140,60], ok:[0,120,40], err:[180,30,30],\n" " warn:[120,140,0], link:[0,80,180],\n" " pad:[210,245,210], padSharp:[190,230,190], padLine:[140,200,140],\n" " cursor:[0,120,40] }\n" " },\n" " ember: {\n" " label: 'ember', desc: 'warm amber',\n" " dark: { bg:[20,12,8], bgAlt:[28,18,12], bgDim:[14,8,5],\n" " fg:220, fgDim:150, fgMute:90,\n" " bar:[35,20,12], border:[60,35,20],\n" " accent:[255,140,40], ok:[120,220,80], err:[255,70,50],\n" " warn:[255,200,60], link:[255,180,100],\n" " pad:[28,18,12], padSharp:[18,10,6], padLine:[55,35,22],\n" " cursor:[255,120,30] },\n" " light: { bg:[255,245,230], bgAlt:[255,250,240], bgDim:[245,235,218],\n" " fg:40, fgDim:90, fgMute:140,\n" " bar:[245,232,215], border:[210,190,160],\n" " accent:[200,100,20], ok:[40,140,50], err:[190,40,30],\n" " warn:[180,120,20], link:[180,90,20],\n" " pad:[250,240,225], padSharp:[238,225,208], padLine:[220,200,175],\n" " cursor:[200,90,15] }\n" " }\n" " };\n" " // Apply a named preset (or 'default' to clear override)\n" " t.apply = function(id) {\n" " if (!id || id === 'default') {\n" " t._overrideId = null;\n" " t._override = null;\n" " } else if (t.presets[id]) {\n" " t._overrideId = id;\n" " t._override = t.presets[id];\n" " }\n" " t._lastCheck = 0;\n" " t.update();\n" " };\n" " t.update = function() {\n" " var now = Date.now();\n" " if (now - t._lastCheck < 5000) return t;\n" " t._lastCheck = now;\n" " var h = getLAHour();\n" " t.dark = (t._forceDark !== undefined) ? !!t._forceDark : (h >= 20 || h < 7);\n" " t.hour = h;\n" " // Backgrounds\n" " t.bg = t.dark ? [20, 20, 25] : [240, 238, 232];\n" " t.bgAlt = t.dark ? [28, 28, 30] : [250, 248, 244];\n" " t.bgDim = t.dark ? [15, 15, 18] : [230, 228, 222];\n" " // Foregrounds\n" " t.fg = t.dark ? 220 : 40;\n" " t.fgDim = t.dark ? 140 : 100;\n" " t.fgMute = t.dark ? 80 : 150;\n" " // UI elements\n" " t.bar = t.dark ? [35, 20, 30] : [225, 220, 215];\n" " t.border = t.dark ? [55, 35, 45] : [200, 195, 190];\n" " t.accent = t.dark ? [200, 100, 140] : [180, 60, 120];\n" " t.ok = t.dark ? [80, 255, 120] : [30, 160, 60];\n" " t.err = t.dark ? [255, 85, 85] : [200, 40, 40];\n" " t.warn = t.dark ? [255, 200, 60] : [180, 120, 20];\n" " t.link = t.dark ? [120, 200, 255] : [40, 100, 200];\n" " // Pad colors (for notepat-style grids)\n" " t.pad = t.dark ? [28, 28, 30] : [250, 248, 244];\n" " t.padSharp = t.dark ? [18, 18, 20] : [235, 232, 228];\n" " t.padLine = t.dark ? [50, 50, 55] : [210, 205, 200];\n" " // Cursor\n" " t.cursor = t.dark ? [220, 80, 140] : [180, 50, 110];\n" " // Apply preset override if active\n" " if (t._override) {\n" " var m = t.dark ? t._override.dark : t._override.light;\n" " if (m) { for (var k in m) { t[k] = m[k]; } }\n" " }\n" " return t;\n" " };\n" " t.update();\n" " return t;\n" "})();\n" ;
ACRuntime *js_init(ACGraph *graph, ACInput *input, ACAudio *audio, ACWifi *wifi, ACTts *tts) { ACRuntime *rt = calloc(1, sizeof(ACRuntime)); if (!rt) return NULL;
rt->graph = graph; rt->input = input; rt->audio = audio; rt->wifi = wifi; rt->tts = tts; rt->ws = ws_create(); rt->udp = udp_create(); rt->boot_fn = JS_UNDEFINED; rt->paint_fn = JS_UNDEFINED; rt->act_fn = JS_UNDEFINED; rt->sim_fn = JS_UNDEFINED; rt->leave_fn = JS_UNDEFINED; rt->beat_fn = JS_UNDEFINED;
// Initialize 3D camera camera3d_init(&rt->camera3d);
rt->rt = JS_NewRuntime(); rt->ctx = JS_NewContext(rt->rt);
// Register Form and Painting classes JS_NewClassID(&form_class_id); JS_NewClass(rt->rt, form_class_id, &form_class_def); JS_NewClassID(&painting_class_id); JS_NewClass(rt->rt, painting_class_id, &painting_class_def);
// Set module loader JS_SetModuleLoaderFunc(rt->rt, NULL, js_module_loader, NULL);
current_rt = rt;
JSContext *ctx = rt->ctx; JSValue global = JS_GetGlobalObject(ctx);
// Create the chainable paint API object (with 3D .form() and .ink() support) JSValue paint_api = JS_NewObject(ctx); JS_SetPropertyStr(ctx, paint_api, "box", JS_NewCFunction(ctx, js_box, "box", 5)); JS_SetPropertyStr(ctx, paint_api, "line", JS_NewCFunction(ctx, js_line, "line", 4)); JS_SetPropertyStr(ctx, paint_api, "circle", JS_NewCFunction(ctx, js_circle, "circle", 4)); JS_SetPropertyStr(ctx, paint_api, "plot", JS_NewCFunction(ctx, js_plot, "plot", 2)); JS_SetPropertyStr(ctx, paint_api, "write", JS_NewCFunction(ctx, js_chain_write, "write", 6)); JS_SetPropertyStr(ctx, paint_api, "scroll", JS_NewCFunction(ctx, js_scroll, "scroll", 2)); JS_SetPropertyStr(ctx, paint_api, "blur", JS_NewCFunction(ctx, js_blur, "blur", 1)); JS_SetPropertyStr(ctx, paint_api, "zoom", JS_NewCFunction(ctx, js_zoom, "zoom", 1)); JS_SetPropertyStr(ctx, paint_api, "contrast", JS_NewCFunction(ctx, js_contrast, "contrast", 1)); JS_SetPropertyStr(ctx, paint_api, "spin", JS_NewCFunction(ctx, js_spin, "spin", 1)); JS_SetPropertyStr(ctx, paint_api, "qr", JS_NewCFunction(ctx, js_qr, "qr", 4)); // 3D chain methods JS_SetPropertyStr(ctx, paint_api, "form", JS_NewCFunction(ctx, js_chain_form, "form", 1)); JS_SetPropertyStr(ctx, paint_api, "ink", JS_NewCFunction(ctx, js_chain_ink, "ink", 4)); // pppline — polyline through array of {x,y} points (used by line.mjs for 1px strokes) { const char *pppline_src = "(function(pts) {\n" " if (!pts || pts.length < 2) return;\n" " for (var i = 0; i < pts.length - 1; i++)\n" " line(pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y);\n" "})"; JSValue pppline_fn = JS_Eval(ctx, pppline_src, strlen(pppline_src), "<pppline>", JS_EVAL_TYPE_GLOBAL); JS_SetPropertyStr(ctx, paint_api, "pppline", pppline_fn); } JS_SetPropertyStr(ctx, global, "__paintApi", JS_DupValue(ctx, paint_api)); JS_FreeValue(ctx, paint_api);
// Register top-level graphics functions JS_SetPropertyStr(ctx, global, "wipe", JS_NewCFunction(ctx, js_wipe, "wipe", 3)); JS_SetPropertyStr(ctx, global, "ink", JS_NewCFunction(ctx, js_ink, "ink", 4)); JS_SetPropertyStr(ctx, global, "line", JS_NewCFunction(ctx, js_line, "line", 5)); JS_SetPropertyStr(ctx, global, "box", JS_NewCFunction(ctx, js_box, "box", 5)); JS_SetPropertyStr(ctx, global, "circle", JS_NewCFunction(ctx, js_circle, "circle", 4)); JS_SetPropertyStr(ctx, global, "qr", JS_NewCFunction(ctx, js_qr, "qr", 4)); JS_SetPropertyStr(ctx, global, "plot", JS_NewCFunction(ctx, js_plot, "plot", 2)); JS_SetPropertyStr(ctx, global, "write", JS_NewCFunction(ctx, js_write, "write", 6)); JS_SetPropertyStr(ctx, global, "scroll", JS_NewCFunction(ctx, js_scroll, "scroll", 2)); JS_SetPropertyStr(ctx, global, "blur", JS_NewCFunction(ctx, js_blur, "blur", 1)); JS_SetPropertyStr(ctx, global, "zoom", JS_NewCFunction(ctx, js_zoom, "zoom", 1)); JS_SetPropertyStr(ctx, global, "contrast", JS_NewCFunction(ctx, js_contrast, "contrast", 1)); JS_SetPropertyStr(ctx, global, "spin", JS_NewCFunction(ctx, js_spin, "spin", 1));
// console.log JSValue console_obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, console_obj, "log", JS_NewCFunction(ctx, js_console_log, "log", 1)); JS_SetPropertyStr(ctx, console_obj, "warn", JS_NewCFunction(ctx, js_console_log, "warn", 1)); JS_SetPropertyStr(ctx, console_obj, "error", JS_NewCFunction(ctx, js_console_log, "error", 1)); JS_SetPropertyStr(ctx, global, "console", console_obj);
// performance.now() { JSValue perf = JS_NewObject(ctx); JS_SetPropertyStr(ctx, perf, "now", JS_NewCFunction(ctx, js_performance_now, "now", 0)); JS_SetPropertyStr(ctx, global, "performance", perf); }
// Run init JS code (Button, Box classes) JSValue init_result = JS_Eval(ctx, js_init_code, strlen(js_init_code), "<init>", JS_EVAL_TYPE_GLOBAL); if (JS_IsException(init_result)) { JSValue exc = JS_GetException(ctx); const char *str = JS_ToCString(ctx, exc); fprintf(stderr, "[js] Init code error: %s\n", str); JS_FreeCString(ctx, str); JS_FreeValue(ctx, exc); } JS_FreeValue(ctx, init_result);
// Browser API stubs for KidLisp evaluator compatibility static const char *browser_stubs = "if(typeof window==='undefined'){" " globalThis.window={" " location:{href:'',origin:'',hostname:'localhost',pathname:'/',search:'',hash:''}," " history:{pushState:function(){},replaceState:function(){}}," " matchMedia:function(){return{matches:false,addEventListener:function(){}}}," " addEventListener:function(){}," " postMessage:function(){}," " navigator:{userAgent:'ac-native'}," " document:{createElement:function(){return{style:{},getContext:function(){return{}},setAttribute:function(){}}}}," " origin:''" " };" " globalThis.document=window.document;" " globalThis.navigator=window.navigator;" " globalThis.localStorage={_s:{},getItem:function(k){return this._s[k]||null},setItem:function(k,v){this._s[k]=v},removeItem:function(k){delete this._s[k]}};" " globalThis.fetch=function(){return Promise.resolve({ok:false,json:function(){return Promise.resolve({})}})};" " globalThis.indexedDB=null;" " globalThis.requestAnimationFrame=function(cb){cb(performance.now());return 0};" " globalThis.cancelAnimationFrame=function(){};" " globalThis.Image=function(){this.src='';this.onload=null};" "}"; JSValue stubs_result = JS_Eval(ctx, browser_stubs, strlen(browser_stubs), "<browser-stubs>", JS_EVAL_TYPE_GLOBAL); if (JS_IsException(stubs_result)) { JSValue exc = JS_GetException(ctx); const char *str = JS_ToCString(ctx, exc); fprintf(stderr, "[js] Browser stubs error: %s\n", str); JS_FreeCString(ctx, str); JS_FreeValue(ctx, exc); } JS_FreeValue(ctx, stubs_result);
// Load KidLisp evaluator bundle (IIFE → globalThis.KidLispModule) FILE *kl_f = fopen("/jslib/kidlisp-bundle.js", "r"); if (kl_f) { fseek(kl_f, 0, SEEK_END); long kl_len = ftell(kl_f); fseek(kl_f, 0, SEEK_SET); char *kl_src = malloc(kl_len + 1); fread(kl_src, 1, kl_len, kl_f); kl_src[kl_len] = '\0'; fclose(kl_f); JSValue kl_result = JS_Eval(ctx, kl_src, kl_len, "<kidlisp-bundle>", JS_EVAL_TYPE_GLOBAL); free(kl_src); if (JS_IsException(kl_result)) { JSValue exc = JS_GetException(ctx); const char *str = JS_ToCString(ctx, exc); fprintf(stderr, "[js] KidLisp bundle error: %s\n", str); JS_FreeCString(ctx, str); JS_FreeValue(ctx, exc); } else { fprintf(stderr, "[js] KidLisp evaluator loaded (%ld bytes)\n", kl_len); } JS_FreeValue(ctx, kl_result); } else { fprintf(stderr, "[js] KidLisp bundle not found at /jslib/kidlisp-bundle.js (optional)\n"); }
// Load FPS system bundle (Camera + Dolly + CamDoll as globalThis.__FpsSystem) // so pieces that export `system = "fps"` (arena.mjs etc.) can be wrapped // synchronously in the piece-load shim without async module resolution. FILE *fps_f = fopen("/jslib/fps-system-bundle.js", "r"); if (fps_f) { fseek(fps_f, 0, SEEK_END); long fps_len = ftell(fps_f); fseek(fps_f, 0, SEEK_SET); char *fps_src = malloc(fps_len + 1); fread(fps_src, 1, fps_len, fps_f); fps_src[fps_len] = '\0'; fclose(fps_f); JSValue fps_result = JS_Eval(ctx, fps_src, fps_len, "<fps-system-bundle>", JS_EVAL_TYPE_GLOBAL); free(fps_src); if (JS_IsException(fps_result)) { JSValue exc = JS_GetException(ctx); const char *str = JS_ToCString(ctx, exc); fprintf(stderr, "[js] FPS bundle error: %s\n", str); JS_FreeCString(ctx, str); JS_FreeValue(ctx, exc); } else { fprintf(stderr, "[js] FPS system loaded (%ld bytes)\n", fps_len); } JS_FreeValue(ctx, fps_result); } else { fprintf(stderr, "[js] FPS bundle not found at /jslib/fps-system-bundle.js (optional)\n"); }
JS_FreeValue(ctx, global); return rt;}
// painting(w, h, callback) — create a real off-screen framebuffer for textures// Calls the callback with a paint API that draws into the off-screen bufferstatic JSValue js_painting(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; int w = 100, h = 100; if (argc >= 1) JS_ToInt32(ctx, &w, argv[0]); if (argc >= 2) JS_ToInt32(ctx, &h, argv[1]);
// Create a real off-screen framebuffer ACFramebuffer *tex_fb = fb_create(w, h); if (!tex_fb) return JS_EXCEPTION;
// Create JS painting object with opaque ACFramebuffer* JSValue painting = JS_NewObjectClass(ctx, painting_class_id); JS_SetOpaque(painting, tex_fb); JS_SetPropertyStr(ctx, painting, "width", JS_NewInt32(ctx, w)); JS_SetPropertyStr(ctx, painting, "height", JS_NewInt32(ctx, h));
// Expose pixels as a Uint8Array view into the framebuffer memory. // Nopaint uses buffer.pixels[i] to adjust alpha during bake. // Note: pixel format is ARGB32 (native byte order). { int byte_len = tex_fb->stride * h * 4; JSValue ab = JS_NewArrayBuffer(ctx, (uint8_t *)tex_fb->pixels, byte_len, NULL, NULL, 0); // No free — painting finalizer handles the framebuffer // Construct Uint8Array from the ArrayBuffer via JS eval JSValue global = JS_GetGlobalObject(ctx); JSValue uint8_ctor = JS_GetPropertyStr(ctx, global, "Uint8Array"); JSValue pixels = JS_CallConstructor(ctx, uint8_ctor, 1, &ab); JS_SetPropertyStr(ctx, painting, "pixels", pixels); JS_FreeValue(ctx, uint8_ctor); JS_FreeValue(ctx, global); JS_FreeValue(ctx, ab); }
// If callback provided, render into the off-screen buffer if (argc >= 3 && JS_IsFunction(ctx, argv[2])) { // Save current render target and switch to off-screen ACFramebuffer *saved_fb = current_rt->graph->fb; graph_page(current_rt->graph, tex_fb);
JSValue global = JS_GetGlobalObject(ctx); JSValue paint_api = JS_NewObject(ctx); JS_SetPropertyStr(ctx, paint_api, "wipe", JS_GetPropertyStr(ctx, global, "wipe")); JS_SetPropertyStr(ctx, paint_api, "ink", JS_GetPropertyStr(ctx, global, "ink")); JS_SetPropertyStr(ctx, paint_api, "box", JS_GetPropertyStr(ctx, global, "box")); JS_SetPropertyStr(ctx, paint_api, "line", JS_GetPropertyStr(ctx, global, "line")); JS_SetPropertyStr(ctx, paint_api, "circle", JS_GetPropertyStr(ctx, global, "circle")); JS_SetPropertyStr(ctx, paint_api, "plot", JS_GetPropertyStr(ctx, global, "plot")); JS_SetPropertyStr(ctx, paint_api, "write", JS_GetPropertyStr(ctx, global, "write")); JS_SetPropertyStr(ctx, paint_api, "kidlisp", JS_NewCFunction(ctx, js_noop, "kidlisp", 5)); JS_FreeValue(ctx, global);
JSValue result = JS_Call(ctx, argv[2], JS_UNDEFINED, 1, &paint_api); JS_FreeValue(ctx, result); JS_FreeValue(ctx, paint_api);
// Restore previous render target graph_page(current_rt->graph, saved_fb); }
return painting;}
// page(painting) — switch render target to a painting buffer (or back to screen)// page(painting) returns an object with wipe/ink/box/line/etc that draw into that buffer.// In AC web, page() returns a chainable proxy. Here we just switch the graph target.static JSValue js_page(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->graph) return JS_UNDEFINED;
if (argc < 1 || JS_IsUndefined(argv[0]) || JS_IsNull(argv[0])) { // page() with no args or page(screen) — restore screen target graph_page(current_rt->graph, current_rt->graph->screen); } else { // page(painting) — switch to painting buffer ACFramebuffer *fb = JS_GetOpaque(argv[0], painting_class_id); if (fb) graph_page(current_rt->graph, fb); }
// Return a page proxy object with wipe() so callers can do page(buf).wipe(...) JSValue proxy = JS_NewObject(ctx); JSValue global = JS_GetGlobalObject(ctx); JS_SetPropertyStr(ctx, proxy, "wipe", JS_GetPropertyStr(ctx, global, "wipe")); JS_SetPropertyStr(ctx, proxy, "ink", JS_GetPropertyStr(ctx, global, "ink")); JS_SetPropertyStr(ctx, proxy, "box", JS_GetPropertyStr(ctx, global, "box")); JS_SetPropertyStr(ctx, proxy, "line", JS_GetPropertyStr(ctx, global, "line")); JS_SetPropertyStr(ctx, proxy, "circle", JS_GetPropertyStr(ctx, global, "circle")); JS_SetPropertyStr(ctx, proxy, "plot", JS_GetPropertyStr(ctx, global, "plot")); JS_SetPropertyStr(ctx, proxy, "write", JS_GetPropertyStr(ctx, global, "write")); JS_FreeValue(ctx, global); return proxy;}
// paste(painting, dx?, dy?) — alpha-composite a painting onto the current render targetstatic JSValue js_paste(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->graph) return JS_UNDEFINED; if (argc < 1) return JS_UNDEFINED;
ACFramebuffer *src = JS_GetOpaque(argv[0], painting_class_id); if (!src) return JS_UNDEFINED;
int dx = 0, dy = 0; if (argc >= 2) JS_ToInt32(ctx, &dx, argv[1]); if (argc >= 3) JS_ToInt32(ctx, &dy, argv[2]);
graph_paste(current_rt->graph, src, dx, dy); return JS_UNDEFINED;}
// system.consumePromptCmd() — pop a pending outside-in prompt command staged// by ac-native.c. Returns {id, text, bg, returnTo} once per arrival and// clears the slot, or null if nothing's queued. `bg=true` means run silently// without changing pieces; `returnTo` is the piece the device was on before// being routed through prompt for this evaluation.static JSValue js_consume_prompt_cmd(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt || !current_rt->pending_prompt_cmd) return JS_NULL; JSValue obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, obj, "id", JS_NewString(ctx, current_rt->pending_prompt_id)); JS_SetPropertyStr(ctx, obj, "text", JS_NewString(ctx, current_rt->pending_prompt_text)); JS_SetPropertyStr(ctx, obj, "bg", JS_NewBool(ctx, current_rt->pending_prompt_bg)); JS_SetPropertyStr(ctx, obj, "returnTo", JS_NewString(ctx, current_rt->pending_prompt_return_to)); current_rt->pending_prompt_cmd = 0; current_rt->pending_prompt_text[0] = 0; current_rt->pending_prompt_id[0] = 0; current_rt->pending_prompt_bg = 0; current_rt->pending_prompt_return_to[0] = 0; return obj;}
// system.machinesResponse(id, output, ok?) — ship a command-response back// through the existing /machines WebSocket session so the viewer's UI can// render what the device produced for an outside-in prompt.static JSValue js_machines_response(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2) return JS_FALSE; const char *id = JS_ToCString(ctx, argv[0]); const char *output = JS_ToCString(ctx, argv[1]); int ok = 1; if (argc >= 3) ok = JS_ToBool(ctx, argv[2]); if (id && output) { machines_send_response(&g_machines, id, "prompt", output, ok); } if (id) JS_FreeCString(ctx, id); if (output) JS_FreeCString(ctx, output); return JS_TRUE;}
// system.raylibTest(painting, frame?) — fill a painting buffer with a// raylib-rendered test pattern (CPU-side software path). Returns true on// success, false when raylib isn't compiled in or the painting is invalid.static JSValue js_raylib_test(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_FALSE; ACFramebuffer *fb = JS_GetOpaque(argv[0], painting_class_id); if (!fb || !fb->pixels) return JS_FALSE; int frame = 0; if (argc >= 2) JS_ToInt32(ctx, &frame, argv[1]);#ifdef HAVE_RAYLIB int rc = raylib_soft_test(fb->pixels, fb->width, fb->height, fb->stride, frame); return rc == 0 ? JS_TRUE : JS_FALSE;#else (void)fb; (void)frame; return JS_FALSE;#endif}
// sound.bpm(val?) — get or set BPM, returns current valuestatic JSValue js_sound_bpm(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio) return JS_NewFloat64(ctx, 120.0); if (argc >= 1 && JS_IsNumber(argv[0])) { double val; JS_ToFloat64(ctx, &val, argv[0]); if (val > 0) audio->bpm = val; } return JS_NewFloat64(ctx, audio->bpm);}
// Drum-skin friction: `sound.scratch({level, cutoff, resonance, roughness,// release, pan, synthetic})` while a finger slides on the pad, and// `sound.scratch(null)` (or level 0) when it lifts. One continuous voice, so// the piece re-states it every control frame rather than starting notes.//// The material mapping — where on the head, how fast, how many fingers — is// the piece's job (see padScratch in pieces/notepat.mjs), matching how// MenuBandPercussion splits control from render on macOS.static JSValue js_sound_scratch(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; ACAudio *audio = current_rt->audio; if (!audio) return JS_UNDEFINED; if (argc < 1 || !JS_IsObject(argv[0])) { audio_scratch_stop(audio); return JS_UNDEFINED; } double level = 0, cutoff = 1600, resonance = 150, roughness = 0.5; double release = 0.014, pan = 0; int synthetic = 0; JSValue v;#define SCRATCH_NUM(name, dest) \ v = JS_GetPropertyStr(ctx, argv[0], name); \ if (JS_IsNumber(v)) JS_ToFloat64(ctx, &dest, v); \ JS_FreeValue(ctx, v); SCRATCH_NUM("level", level) SCRATCH_NUM("cutoff", cutoff) SCRATCH_NUM("resonance", resonance) SCRATCH_NUM("roughness", roughness) SCRATCH_NUM("release", release) SCRATCH_NUM("pan", pan)#undef SCRATCH_NUM v = JS_GetPropertyStr(ctx, argv[0], "synthetic"); if (!JS_IsUndefined(v)) synthetic = JS_ToBool(ctx, v); JS_FreeValue(ctx, v);
if (level <= 0.0) audio_scratch_stop(audio); else audio_scratch_set(audio, level, cutoff, resonance, roughness, release, pan, synthetic); return JS_UNDEFINED;}
// --- DJ deck bindings ---
static void blit_argb_nearest(ACFramebuffer *fb, const uint32_t *src, int sw, int sh, int dx, int dy, int dw, int dh) { if (!fb || !src || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0) return;
int x0 = dx < 0 ? 0 : dx; int y0 = dy < 0 ? 0 : dy; int x1 = dx + dw; int y1 = dy + dh; if (x1 > fb->width) x1 = fb->width; if (y1 > fb->height) y1 = fb->height; if (x0 >= x1 || y0 >= y1) return;
for (int y = y0; y < y1; y++) { int sy = (int)(((int64_t)(y - dy) * sh) / dh); if (sy < 0) sy = 0; if (sy >= sh) sy = sh - 1; const uint32_t *src_row = src + sy * sw; uint32_t *dst_row = fb->pixels + y * fb->stride; for (int x = x0; x < x1; x++) { int sx = (int)(((int64_t)(x - dx) * sw) / dw); if (sx < 0) sx = 0; if (sx >= sw) sx = sw - 1; uint32_t px = src_row[sx]; if ((px >> 24) == 0) continue; dst_row[x] = px; } }}
// sound.deck.load(deck, path) -> true/falsestatic JSValue js_deck_load(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2 || !current_rt || !current_rt->audio) return JS_FALSE; int deck; JS_ToInt32(ctx, &deck, argv[0]); const char *path = JS_ToCString(ctx, argv[1]); if (!path) return JS_FALSE; int ret = audio_deck_load(current_rt->audio, deck, path); JS_FreeCString(ctx, path); return JS_NewBool(ctx, ret == 0);}
// sound.deck.play(deck)static JSValue js_deck_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; int deck; JS_ToInt32(ctx, &deck, argv[0]); audio_deck_play(current_rt->audio, deck); return JS_UNDEFINED;}
// sound.deck.pause(deck)static JSValue js_deck_pause(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; int deck; JS_ToInt32(ctx, &deck, argv[0]); audio_deck_pause(current_rt->audio, deck); return JS_UNDEFINED;}
// sound.deck.seek(deck, seconds)static JSValue js_deck_seek(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2 || !current_rt || !current_rt->audio) return JS_UNDEFINED; int deck; JS_ToInt32(ctx, &deck, argv[0]); double sec; JS_ToFloat64(ctx, &sec, argv[1]); audio_deck_seek(current_rt->audio, deck, sec); return JS_UNDEFINED;}
// sound.deck.setSpeed(deck, speed)static JSValue js_deck_set_speed(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2 || !current_rt || !current_rt->audio) return JS_UNDEFINED; int deck; JS_ToInt32(ctx, &deck, argv[0]); double spd; JS_ToFloat64(ctx, &spd, argv[1]); audio_deck_set_speed(current_rt->audio, deck, spd); return JS_UNDEFINED;}
// sound.deck.getPeaks(deck) — returns Float32Array of normalized peak amplitudesstatic JSValue js_deck_get_peaks(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt || !current_rt->audio) return JS_NULL; int deck; JS_ToInt32(ctx, &deck, argv[0]); if (deck < 0 || deck >= AUDIO_MAX_DECKS) return JS_NULL; ACDeck *dk = ¤t_rt->audio->decks[deck]; if (!dk->decoder || !dk->decoder->peaks || dk->decoder->peak_count <= 0) return JS_NULL;
// Build a JS array from the peak data JSValue arr = JS_NewArray(ctx); for (int i = 0; i < dk->decoder->peak_count; i++) { JS_SetPropertyUint32(ctx, arr, i, JS_NewFloat64(ctx, dk->decoder->peaks[i])); } return arr;}
// sound.deck.prepareVideo(deck[, width, height, fps]) — decode a low-res preview stripstatic JSValue js_deck_prepare_video(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt || !current_rt->audio) return JS_FALSE; int deck = 0, width = 96, height = 54, fps = 12; JS_ToInt32(ctx, &deck, argv[0]); if (argc > 1) JS_ToInt32(ctx, &width, argv[1]); if (argc > 2) JS_ToInt32(ctx, &height, argv[2]); if (argc > 3) JS_ToInt32(ctx, &fps, argv[3]); if (deck < 0 || deck >= AUDIO_MAX_DECKS) return JS_FALSE; ACDeck *dk = ¤t_rt->audio->decks[deck]; if (!dk->decoder) return JS_FALSE; int ret = deck_decoder_generate_video_preview(dk->decoder, width, height, fps); return JS_NewBool(ctx, ret > 0);}
// sound.deck.videoBlit(deck, x, y, w, h) — draw current preview frame into the framebufferstatic JSValue js_deck_video_blit(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 5 || !current_rt || !current_rt->audio || !current_rt->graph || !current_rt->graph->fb) return JS_FALSE;
int deck = 0, x = 0, y = 0, w = 0, h = 0; JS_ToInt32(ctx, &deck, argv[0]); JS_ToInt32(ctx, &x, argv[1]); JS_ToInt32(ctx, &y, argv[2]); JS_ToInt32(ctx, &w, argv[3]); JS_ToInt32(ctx, &h, argv[4]); if (deck < 0 || deck >= AUDIO_MAX_DECKS) return JS_FALSE;
ACDeck *dk = ¤t_rt->audio->decks[deck]; if (!dk->decoder || !dk->decoder->video_ready || !dk->decoder->video_frames || dk->decoder->video_frame_count <= 0 || dk->decoder->video_width <= 0 || dk->decoder->video_height <= 0 || dk->decoder->video_fps <= 0.0) { return JS_FALSE; }
if (w <= 0) w = dk->decoder->video_width; if (h <= 0) h = dk->decoder->video_height;
int idx = (int)floor(dk->decoder->position * dk->decoder->video_fps + 0.0001); if (idx < 0) idx = 0; if (idx >= dk->decoder->video_frame_count) idx = dk->decoder->video_frame_count - 1;
size_t pixels_per_frame = (size_t)dk->decoder->video_width * (size_t)dk->decoder->video_height; const uint32_t *src = dk->decoder->video_frames + ((size_t)idx * pixels_per_frame); blit_argb_nearest(current_rt->graph->fb, src, dk->decoder->video_width, dk->decoder->video_height, x, y, w, h); return JS_TRUE;}
// sound.deck.setVolume(deck, vol)static JSValue js_deck_set_volume(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2 || !current_rt || !current_rt->audio) return JS_UNDEFINED; int deck; JS_ToInt32(ctx, &deck, argv[0]); double vol; JS_ToFloat64(ctx, &vol, argv[1]); audio_deck_set_volume(current_rt->audio, deck, (float)vol); return JS_UNDEFINED;}
// sound.deck.setCrossfader(value)static JSValue js_deck_set_crossfader(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; double val; JS_ToFloat64(ctx, &val, argv[0]); audio_deck_set_crossfader(current_rt->audio, (float)val); return JS_UNDEFINED;}
// sound.deck.setMasterVolume(value)static JSValue js_deck_set_master_vol(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt || !current_rt->audio) return JS_UNDEFINED; double val; JS_ToFloat64(ctx, &val, argv[0]); audio_deck_set_master_volume(current_rt->audio, (float)val); return JS_UNDEFINED;}
// Build the sound object for the APIstatic JSValue build_sound_obj(JSContext *ctx, ACRuntime *rt) { JSValue sound = JS_NewObject(ctx);
// Core synthesis functions JS_SetPropertyStr(ctx, sound, "synth", JS_NewCFunction(ctx, js_synth, "synth", 1)); JS_SetPropertyStr(ctx, sound, "freq", JS_NewCFunction(ctx, js_sound_freq, "freq", 1)); JS_SetPropertyStr(ctx, sound, "play", JS_NewCFunction(ctx, js_noop, "play", 2)); JS_SetPropertyStr(ctx, sound, "kill", JS_NewCFunction(ctx, js_sound_kill, "kill", 2)); JS_SetPropertyStr(ctx, sound, "update", JS_NewCFunction(ctx, js_sound_update, "update", 2)); JS_SetPropertyStr(ctx, sound, "bpm", JS_NewCFunction(ctx, js_sound_bpm, "bpm", 1)); JS_SetPropertyStr(ctx, sound, "scratch", JS_NewCFunction(ctx, js_sound_scratch, "scratch", 1)); JS_SetPropertyStr(ctx, sound, "time", JS_NewFloat64(ctx, rt->audio ? rt->audio->time : 0.0)); JS_SetPropertyStr(ctx, sound, "registerSample", JS_NewCFunction(ctx, js_noop, "registerSample", 3));
// speaker sub-object JSValue speaker = JS_NewObject(ctx); JS_SetPropertyStr(ctx, speaker, "poll", JS_NewCFunction(ctx, js_noop, "poll", 0)); JS_SetPropertyStr(ctx, speaker, "getRecentBuffer", JS_NewCFunction(ctx, js_speaker_get_recent_buffer, "getRecentBuffer", 1)); JS_SetPropertyStr(ctx, speaker, "setCapturePaused", JS_NewCFunction(ctx, js_speaker_set_capture_paused, "setCapturePaused", 1)); JS_SetPropertyStr(ctx, speaker, "drawStrip", JS_NewCFunction(ctx, js_speaker_draw_strip, "drawStrip", 7)); JS_SetPropertyStr(ctx, speaker, "sampleRate", JS_NewInt32(ctx, rt->audio ? (int)rt->audio->actual_rate : AUDIO_SAMPLE_RATE));
// sound.tape — read-only tape recorder state. The actual start/stop // is driven by the PrintScreen key handler in ac-native.c (which // owns the MP4 file lifecycle, TTS announce, on-screen overlay, and // cloud upload). JS just observes. JSValue tape = JS_NewObject(ctx); JS_SetPropertyStr(ctx, tape, "recording", JS_NewCFunction(ctx, js_tape_recording, "recording", 0)); JS_SetPropertyStr(ctx, tape, "elapsed", JS_NewCFunction(ctx, js_tape_elapsed, "elapsed", 0)); JS_SetPropertyStr(ctx, sound, "tape", tape);
// waveforms (32 samples, left channel only — sufficient for visualizer) JSValue waveforms = JS_NewObject(ctx); JSValue wf_left = JS_NewArray(ctx); if (rt->audio) { for (int i = 0; i < 32; i++) { int idx = (rt->audio->waveform_pos - 32 + i + AUDIO_WAVEFORM_SIZE) % AUDIO_WAVEFORM_SIZE; JS_SetPropertyUint32(ctx, wf_left, i, JS_NewFloat64(ctx, rt->audio->waveform_left[idx])); } } JS_SetPropertyStr(ctx, waveforms, "left", wf_left); JS_SetPropertyStr(ctx, speaker, "waveforms", waveforms);
// amplitudes JSValue amplitudes = JS_NewObject(ctx); JS_SetPropertyStr(ctx, amplitudes, "left", JS_NewFloat64(ctx, rt->audio ? rt->audio->amplitude_left : 0.0)); JS_SetPropertyStr(ctx, amplitudes, "right", JS_NewFloat64(ctx, rt->audio ? rt->audio->amplitude_right : 0.0)); JS_SetPropertyStr(ctx, speaker, "amplitudes", amplitudes);
// frequencies (stub) JSValue frequencies = JS_NewObject(ctx); JS_SetPropertyStr(ctx, frequencies, "left", JS_NewArray(ctx)); JS_SetPropertyStr(ctx, speaker, "frequencies", frequencies);
// beat detection (stub) JSValue beat = JS_NewObject(ctx); JS_SetPropertyStr(ctx, beat, "detected", JS_FALSE); JS_SetPropertyStr(ctx, speaker, "beat", beat);
// System volume (0-100) JS_SetPropertyStr(ctx, speaker, "systemVolume", JS_NewInt32(ctx, rt->audio ? rt->audio->system_volume : 100));
JS_SetPropertyStr(ctx, sound, "speaker", speaker);
// room JSValue room = JS_NewObject(ctx); JS_SetPropertyStr(ctx, room, "toggle", JS_NewCFunction(ctx, js_room_toggle, "toggle", 0)); JS_SetPropertyStr(ctx, room, "setMix", JS_NewCFunction(ctx, js_set_room_mix, "setMix", 1)); JS_SetPropertyStr(ctx, room, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->room_mix : 0.0)); JS_SetPropertyStr(ctx, room, "set", JS_NewCFunction(ctx, js_noop, "set", 1)); JS_SetPropertyStr(ctx, room, "get", JS_NewCFunction(ctx, js_noop, "get", 0)); JS_SetPropertyStr(ctx, sound, "room", room);
// glitch JSValue glitch = JS_NewObject(ctx); JS_SetPropertyStr(ctx, glitch, "toggle", JS_NewCFunction(ctx, js_glitch_toggle, "toggle", 0)); JS_SetPropertyStr(ctx, glitch, "setMix", JS_NewCFunction(ctx, js_set_glitch_mix, "setMix", 1)); JS_SetPropertyStr(ctx, glitch, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->glitch_mix : 0.0)); JS_SetPropertyStr(ctx, glitch, "set", JS_NewCFunction(ctx, js_set_glitch_mix, "set", 1)); JS_SetPropertyStr(ctx, glitch, "get", JS_NewCFunction(ctx, js_noop, "get", 0)); JS_SetPropertyStr(ctx, sound, "glitch", glitch);
// fx (dry/wet for entire FX chain) JSValue fx = JS_NewObject(ctx); JS_SetPropertyStr(ctx, fx, "setMix", JS_NewCFunction(ctx, js_set_fx_mix, "setMix", 1)); JS_SetPropertyStr(ctx, fx, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->fx_mix : 1.0)); JS_SetPropertyStr(ctx, sound, "fx", fx);
// volume (user master output gain) JSValue volume = JS_NewObject(ctx); JS_SetPropertyStr(ctx, volume, "setMix", JS_NewCFunction(ctx, js_set_master_volume, "setMix", 1)); JS_SetPropertyStr(ctx, volume, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->master_volume : 1.0)); JS_SetPropertyStr(ctx, volume, "setMono", JS_NewCFunction(ctx, js_set_mono, "setMono", 1)); JS_SetPropertyStr(ctx, volume, "mono", JS_NewBool(ctx, rt->audio ? rt->audio->mono : 0)); JS_SetPropertyStr(ctx, sound, "volume", volume);
// drive (tanh soft-clip saturation) JSValue drive = JS_NewObject(ctx); JS_SetPropertyStr(ctx, drive, "setMix", JS_NewCFunction(ctx, js_set_drive_mix, "setMix", 1)); JS_SetPropertyStr(ctx, drive, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->drive_mix : 0.0)); JS_SetPropertyStr(ctx, sound, "drive", drive);
// wobble (modulated-delay flange) JSValue wobble = JS_NewObject(ctx); JS_SetPropertyStr(ctx, wobble, "setMix", JS_NewCFunction(ctx, js_set_wobble_mix, "setMix", 1)); JS_SetPropertyStr(ctx, wobble, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->wobble_mix : 0.0)); JS_SetPropertyStr(ctx, sound, "wobble", wobble);
// microphone JSValue mic = JS_NewObject(ctx); JS_SetPropertyStr(ctx, mic, "open", JS_NewCFunction(ctx, js_mic_open, "open", 0)); JS_SetPropertyStr(ctx, mic, "close", JS_NewCFunction(ctx, js_mic_close, "close", 0)); JS_SetPropertyStr(ctx, mic, "rec", JS_NewCFunction(ctx, js_mic_rec, "rec", 0)); JS_SetPropertyStr(ctx, mic, "cut", JS_NewCFunction(ctx, js_mic_cut, "cut", 0)); JS_SetPropertyStr(ctx, mic, "recording", JS_NewBool(ctx, rt->audio ? rt->audio->recording : 0)); JS_SetPropertyStr(ctx, mic, "connected", JS_NewBool(ctx, rt->audio ? rt->audio->mic_connected : 0)); JS_SetPropertyStr(ctx, mic, "hot", JS_NewBool(ctx, rt->audio ? rt->audio->mic_hot : 0)); JS_SetPropertyStr(ctx, mic, "sampleLength", JS_NewInt32(ctx, rt->audio ? rt->audio->sample_len : 0)); JS_SetPropertyStr(ctx, mic, "sampleRate", JS_NewInt32(ctx, rt->audio ? (int)rt->audio->sample_rate : 0)); JS_SetPropertyStr(ctx, mic, "level", JS_NewFloat64(ctx, rt->audio ? rt->audio->mic_level : 0.0)); JS_SetPropertyStr(ctx, mic, "lastChunk", JS_NewInt32(ctx, rt->audio ? rt->audio->mic_last_chunk : 0)); JS_SetPropertyStr(ctx, mic, "device", JS_NewString(ctx, (rt->audio && rt->audio->mic_device[0]) ? rt->audio->mic_device : "none")); JS_SetPropertyStr(ctx, mic, "lastError", JS_NewString(ctx, (rt->audio && rt->audio->mic_last_error[0]) ? rt->audio->mic_last_error : "")); // Mic level is already exposed as mic.level (single float, no array overhead) JS_SetPropertyStr(ctx, sound, "microphone", mic);
// sample playback JSValue samp = JS_NewObject(ctx); JS_SetPropertyStr(ctx, samp, "play", JS_NewCFunction(ctx, js_sample_play, "play", 1)); JS_SetPropertyStr(ctx, samp, "kill", JS_NewCFunction(ctx, js_sample_kill, "kill", 2)); JS_SetPropertyStr(ctx, samp, "getData", JS_NewCFunction(ctx, js_sample_get_data, "getData", 0)); JS_SetPropertyStr(ctx, samp, "loadData", JS_NewCFunction(ctx, js_sample_load_data, "loadData", 2)); JS_SetPropertyStr(ctx, samp, "saveTo", JS_NewCFunction(ctx, js_sample_save_to, "saveTo", 1)); JS_SetPropertyStr(ctx, samp, "loadFrom", JS_NewCFunction(ctx, js_sample_load_from, "loadFrom", 1)); JS_SetPropertyStr(ctx, sound, "sample", samp);
// sound.zoo — named one-shot bank (zoo + lasers kits). play() takes // a string name ("dog", "cat", …) and returns a handle that can be // passed to sound.kill() for sustained sounds (e.g. snake hiss). JSValue zoo = JS_NewObject(ctx); JS_SetPropertyStr(ctx, zoo, "play", JS_NewCFunction(ctx, js_zoo_play, "play", 2)); JS_SetPropertyStr(ctx, zoo, "kill", JS_NewCFunction(ctx, js_zoo_kill, "kill", 2)); JS_SetPropertyStr(ctx, sound, "zoo", zoo);
// dedicated global replay voice/buffer JSValue replay = JS_NewObject(ctx); JS_SetPropertyStr(ctx, replay, "play", JS_NewCFunction(ctx, js_replay_play, "play", 1)); JS_SetPropertyStr(ctx, replay, "kill", JS_NewCFunction(ctx, js_replay_kill, "kill", 2)); JS_SetPropertyStr(ctx, replay, "loadData", JS_NewCFunction(ctx, js_replay_load_data, "loadData", 2)); JS_SetPropertyStr(ctx, sound, "replay", replay);
// DJ deck JSValue deck_obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, deck_obj, "load", JS_NewCFunction(ctx, js_deck_load, "load", 2)); JS_SetPropertyStr(ctx, deck_obj, "play", JS_NewCFunction(ctx, js_deck_play, "play", 1)); JS_SetPropertyStr(ctx, deck_obj, "pause", JS_NewCFunction(ctx, js_deck_pause, "pause", 1)); JS_SetPropertyStr(ctx, deck_obj, "seek", JS_NewCFunction(ctx, js_deck_seek, "seek", 2)); JS_SetPropertyStr(ctx, deck_obj, "setSpeed", JS_NewCFunction(ctx, js_deck_set_speed, "setSpeed", 2)); JS_SetPropertyStr(ctx, deck_obj, "setVolume", JS_NewCFunction(ctx, js_deck_set_volume, "setVolume", 2)); JS_SetPropertyStr(ctx, deck_obj, "setCrossfader", JS_NewCFunction(ctx, js_deck_set_crossfader, "setCrossfader", 1)); JS_SetPropertyStr(ctx, deck_obj, "setMasterVolume", JS_NewCFunction(ctx, js_deck_set_master_vol, "setMasterVolume", 1)); JS_SetPropertyStr(ctx, deck_obj, "getPeaks", JS_NewCFunction(ctx, js_deck_get_peaks, "getPeaks", 1)); JS_SetPropertyStr(ctx, deck_obj, "prepareVideo", JS_NewCFunction(ctx, js_deck_prepare_video, "prepareVideo", 4)); JS_SetPropertyStr(ctx, deck_obj, "videoBlit", JS_NewCFunction(ctx, js_deck_video_blit, "videoBlit", 5));
// Deck state (read-only, rebuilt each frame) JSValue decks_arr = JS_NewArray(ctx); ACAudio *aud = rt->audio; for (int d = 0; d < AUDIO_MAX_DECKS; d++) { JSValue di = JS_NewObject(ctx); if (aud) { ACDeck *dk = &aud->decks[d]; JS_SetPropertyStr(ctx, di, "loaded", JS_NewBool(ctx, dk->active)); JS_SetPropertyStr(ctx, di, "playing", JS_NewBool(ctx, dk->playing)); JS_SetPropertyStr(ctx, di, "volume", JS_NewFloat64(ctx, dk->volume)); if (dk->decoder) { JS_SetPropertyStr(ctx, di, "position", JS_NewFloat64(ctx, dk->decoder->position)); JS_SetPropertyStr(ctx, di, "duration", JS_NewFloat64(ctx, dk->decoder->duration)); JS_SetPropertyStr(ctx, di, "speed", JS_NewFloat64(ctx, dk->decoder->speed)); JS_SetPropertyStr(ctx, di, "title", JS_NewString(ctx, dk->decoder->title)); JS_SetPropertyStr(ctx, di, "artist", JS_NewString(ctx, dk->decoder->artist)); JS_SetPropertyStr(ctx, di, "finished", JS_NewBool(ctx, dk->decoder->finished)); JS_SetPropertyStr(ctx, di, "isStream", JS_NewBool(ctx, dk->decoder->is_stream)); JS_SetPropertyStr(ctx, di, "error", JS_NewString(ctx, dk->decoder->error ? dk->decoder->error_msg : "")); JS_SetPropertyStr(ctx, di, "videoReady", JS_NewBool(ctx, dk->decoder->video_ready)); JS_SetPropertyStr(ctx, di, "videoWidth", JS_NewInt32(ctx, dk->decoder->video_width)); JS_SetPropertyStr(ctx, di, "videoHeight", JS_NewInt32(ctx, dk->decoder->video_height)); JS_SetPropertyStr(ctx, di, "videoFps", JS_NewFloat64(ctx, dk->decoder->video_fps)); JS_SetPropertyStr(ctx, di, "videoFrames", JS_NewInt32(ctx, dk->decoder->video_frame_count)); } else { JS_SetPropertyStr(ctx, di, "position", JS_NewFloat64(ctx, 0)); JS_SetPropertyStr(ctx, di, "duration", JS_NewFloat64(ctx, 0)); JS_SetPropertyStr(ctx, di, "speed", JS_NewFloat64(ctx, 1.0)); JS_SetPropertyStr(ctx, di, "title", JS_NewString(ctx, "")); JS_SetPropertyStr(ctx, di, "artist", JS_NewString(ctx, "")); JS_SetPropertyStr(ctx, di, "finished", JS_FALSE); JS_SetPropertyStr(ctx, di, "error", JS_NewString(ctx, "")); JS_SetPropertyStr(ctx, di, "videoReady", JS_FALSE); JS_SetPropertyStr(ctx, di, "videoWidth", JS_NewInt32(ctx, 0)); JS_SetPropertyStr(ctx, di, "videoHeight", JS_NewInt32(ctx, 0)); JS_SetPropertyStr(ctx, di, "videoFps", JS_NewFloat64(ctx, 0)); JS_SetPropertyStr(ctx, di, "videoFrames", JS_NewInt32(ctx, 0)); } } JS_SetPropertyUint32(ctx, decks_arr, d, di); } JS_SetPropertyStr(ctx, deck_obj, "decks", decks_arr); JS_SetPropertyStr(ctx, deck_obj, "crossfaderValue", JS_NewFloat64(ctx, aud ? aud->crossfader : 0.5)); JS_SetPropertyStr(ctx, deck_obj, "masterVolume", JS_NewFloat64(ctx, aud ? aud->deck_master_volume : 0.8)); JS_SetPropertyStr(ctx, sound, "deck", deck_obj);
// TTS JS_SetPropertyStr(ctx, sound, "speak", JS_NewCFunction(ctx, js_speak, "speak", 1)); JS_SetPropertyStr(ctx, sound, "speakVoice", JS_NewCFunction(ctx, js_speak_voice, "speakVoice", 2)); JS_SetPropertyStr(ctx, sound, "speakCached", JS_NewCFunction(ctx, js_speak_cached, "speakCached", 1));
// sound.paint (visualization helpers) JSValue sound_paint = JS_NewObject(ctx); JS_SetPropertyStr(ctx, sound_paint, "bars", JS_NewCFunction(ctx, js_noop, "bars", 10)); JS_SetPropertyStr(ctx, sound_paint, "audioEngineBadge", JS_NewCFunction(ctx, js_noop, "audioEngineBadge", 5)); JS_SetPropertyStr(ctx, sound, "paint", sound_paint);
// midi (stub) JSValue midi = JS_NewObject(ctx); JS_SetPropertyStr(ctx, midi, "connect", JS_NewCFunction(ctx, js_noop, "connect", 0)); JS_SetPropertyStr(ctx, midi, "noteOn", JS_NewCFunction(ctx, js_usb_midi_note_on, "noteOn", 3)); JS_SetPropertyStr(ctx, midi, "noteOff", JS_NewCFunction(ctx, js_usb_midi_note_off, "noteOff", 3)); JS_SetPropertyStr(ctx, midi, "allNotesOff", JS_NewCFunction(ctx, js_usb_midi_all_notes_off, "allNotesOff", 1)); JS_SetPropertyStr(ctx, sound, "midi", midi);
// daw (stub) JSValue daw = JS_NewObject(ctx); JS_SetPropertyStr(ctx, daw, "bpm", JS_UNDEFINED); JS_SetPropertyStr(ctx, sound, "daw", daw);
return sound;}
// Read a small sysfs file into a buffer, return length or -1static int read_sysfs(const char *path, char *buf, int bufsize) { FILE *f = fopen(path, "r"); if (!f) return -1; int len = (int)fread(buf, 1, bufsize - 1, f); fclose(f); if (len > 0 && buf[len-1] == '\n') len--; buf[len] = 0; return len;}
// ============================================================// WiFi JS bindings// ============================================================
static JSValue js_wifi_scan(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (current_rt->wifi) wifi_scan(current_rt->wifi); return JS_UNDEFINED;}
static JSValue js_wifi_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt->wifi || argc < 1) return JS_UNDEFINED; const char *ssid = JS_ToCString(ctx, argv[0]); const char *pass = (argc >= 2 && JS_IsString(argv[1])) ? JS_ToCString(ctx, argv[1]) : NULL; if (ssid) wifi_connect(current_rt->wifi, ssid, pass); if (ssid) JS_FreeCString(ctx, ssid); if (pass) JS_FreeCString(ctx, pass); return JS_UNDEFINED;}
static JSValue js_wifi_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (current_rt->wifi) wifi_disconnect(current_rt->wifi); return JS_UNDEFINED;}
static JSValue build_wifi_obj(JSContext *ctx, ACWifi *wifi) { JSValue obj = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, obj, "scan", JS_NewCFunction(ctx, js_wifi_scan, "scan", 0)); JS_SetPropertyStr(ctx, obj, "connect", JS_NewCFunction(ctx, js_wifi_connect, "connect", 2)); JS_SetPropertyStr(ctx, obj, "disconnect", JS_NewCFunction(ctx, js_wifi_disconnect, "disconnect", 0));
if (wifi) { // State is updated by the wifi worker thread — just read it here JS_SetPropertyStr(ctx, obj, "state", JS_NewInt32(ctx, wifi->state)); JS_SetPropertyStr(ctx, obj, "status", JS_NewString(ctx, wifi->status_msg)); JS_SetPropertyStr(ctx, obj, "connected", JS_NewBool(ctx, wifi->state == WIFI_STATE_CONNECTED)); JS_SetPropertyStr(ctx, obj, "ssid", JS_NewString(ctx, wifi->connected_ssid)); JS_SetPropertyStr(ctx, obj, "ip", JS_NewString(ctx, wifi->ip_address)); JS_SetPropertyStr(ctx, obj, "iface", JS_NewString(ctx, wifi->iface)); JS_SetPropertyStr(ctx, obj, "signal", JS_NewInt32(ctx, wifi->signal_strength));
// Networks array JSValue networks = JS_NewArray(ctx); for (int i = 0; i < wifi->network_count; i++) { JSValue net = JS_NewObject(ctx); JS_SetPropertyStr(ctx, net, "ssid", JS_NewString(ctx, wifi->networks[i].ssid)); JS_SetPropertyStr(ctx, net, "signal", JS_NewInt32(ctx, wifi->networks[i].signal)); JS_SetPropertyStr(ctx, net, "encrypted", JS_NewBool(ctx, wifi->networks[i].encrypted)); JS_SetPropertyUint32(ctx, networks, i, net); } JS_SetPropertyStr(ctx, obj, "networks", networks);
// Log ring buffer (last 32 messages from wifi thread) JSValue logs = JS_NewArray(ctx); int total = wifi->log_count; int count = total < 32 ? total : 32; for (int i = 0; i < count; i++) { // Read in chronological order (oldest first) int idx = (total >= 32) ? ((total - 32 + i) % 32) : i; JS_SetPropertyUint32(ctx, logs, i, JS_NewString(ctx, wifi->log[idx])); } JS_SetPropertyStr(ctx, obj, "logs", logs); } else { extern int wifi_disabled; JS_SetPropertyStr(ctx, obj, "state", JS_NewInt32(ctx, WIFI_STATE_OFF)); JS_SetPropertyStr(ctx, obj, "status", JS_NewString(ctx, wifi_disabled ? "disabled" : "no wifi")); JS_SetPropertyStr(ctx, obj, "disabled", JS_NewBool(ctx, wifi_disabled)); JS_SetPropertyStr(ctx, obj, "connected", JS_FALSE); JS_SetPropertyStr(ctx, obj, "networks", JS_NewArray(ctx)); }
return obj;}
// system.hdmi(r, g, b) — fill secondary display with solid colorstatic JSValue js_hdmi_fill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->hdmi || !current_rt->hdmi->active) return JS_UNDEFINED; if (argc < 3) return JS_UNDEFINED; int r, g, b; JS_ToInt32(ctx, &r, argv[0]); JS_ToInt32(ctx, &g, argv[1]); JS_ToInt32(ctx, &b, argv[2]); drm_secondary_fill(current_rt->hdmi, (uint8_t)r, (uint8_t)g, (uint8_t)b); return JS_UNDEFINED;}
// system.readFile(path, [lastNLines]) — read a file from disk, returns string or null// If lastNLines is provided, returns only the last N lines.static JSValue js_read_file(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_NULL; const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_NULL; int lastN = 0; if (argc >= 2) JS_ToInt32(ctx, &lastN, argv[1]); FILE *fp = fopen(path, "r"); if (!fp) { JS_FreeCString(ctx, path); return JS_NULL; } fseek(fp, 0, SEEK_END); long sz = ftell(fp); if (sz <= 0) { fclose(fp); JS_FreeCString(ctx, path); return JS_NULL; } if (sz > 65536) { fseek(fp, -65536, SEEK_END); sz = 65536; } else { rewind(fp); } char *buf = malloc(sz + 1); if (!buf) { fclose(fp); JS_FreeCString(ctx, path); return JS_NULL; } long rd = (long)fread(buf, 1, sz, fp); buf[rd] = 0; fclose(fp); JS_FreeCString(ctx, path); if (lastN > 0 && rd > 0) { int nl_count = 0; char *p = buf + rd - 1; if (*p == '\n') p--; while (p > buf) { if (*p == '\n') { nl_count++; if (nl_count >= lastN) { p++; break; } } p--; } JSValue result = JS_NewString(ctx, p); free(buf); return result; } JSValue result = JS_NewStringLen(ctx, buf, rd); free(buf); return result;}
// system.readFileBytes(path) — read a file from disk and return its// full contents as an ArrayBuffer. Unlike system.readFile (which is// for text logs and caps at 64 KiB), this returns the entire file// without size cap, suitable for binary formats (PGM, PNG, raw audio).// Returns null on missing/unreadable file.static JSValue js_read_file_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_NULL; const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_NULL; FILE *fp = fopen(path, "rb"); if (!fp) { JS_FreeCString(ctx, path); return JS_NULL; } if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); JS_FreeCString(ctx, path); return JS_NULL; } long sz = ftell(fp); if (sz < 0) { fclose(fp); JS_FreeCString(ctx, path); return JS_NULL; } rewind(fp); JS_FreeCString(ctx, path); if (sz == 0) { fclose(fp); return JS_NewArrayBuffer(ctx, NULL, 0, js_free_array_buffer, NULL, 0); } uint8_t *buf = malloc((size_t)sz); if (!buf) { fclose(fp); return JS_NULL; } size_t rd = fread(buf, 1, (size_t)sz, fp); fclose(fp); if (rd != (size_t)sz) { free(buf); return JS_NULL; } JSValue ab = JS_NewArrayBuffer(ctx, buf, (size_t)sz, js_free_array_buffer, NULL, 0); if (JS_IsException(ab)) { free(buf); return JS_NULL; } return ab;}
// system.writeFile(path, data) — write a string to disk, returns true/falsestatic JSValue js_write_file(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2) return JS_FALSE; const char *path = JS_ToCString(ctx, argv[0]); const char *data = JS_ToCString(ctx, argv[1]); if (!path || !data) { if (path) JS_FreeCString(ctx, path); if (data) JS_FreeCString(ctx, data); return JS_FALSE; } FILE *fp = fopen(path, "w"); int ok = 0; if (fp) { fputs(data, fp); fclose(fp); sync(); // flush to USB ok = 1; } JS_FreeCString(ctx, path); JS_FreeCString(ctx, data); return JS_NewBool(ctx, ok);}
// system.deleteFile(path) — unlink a file, returns true on successstatic JSValue js_delete_file(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_FALSE; const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_FALSE; int ok = (unlink(path) == 0); if (ok) sync(); JS_FreeCString(ctx, path); return JS_NewBool(ctx, ok);}
// system.diskInfo(path) — returns {total, free, available, blockSize, fstype}// for the filesystem containing `path`, or null on error.static JSValue js_disk_info(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_NULL; const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_NULL; struct statvfs vfs; int rc = statvfs(path, &vfs); JS_FreeCString(ctx, path); if (rc != 0) return JS_NULL; JSValue obj = JS_NewObject(ctx); uint64_t bs = (uint64_t)vfs.f_frsize ? (uint64_t)vfs.f_frsize : (uint64_t)vfs.f_bsize; JS_SetPropertyStr(ctx, obj, "total", JS_NewInt64(ctx, (int64_t)(vfs.f_blocks * bs))); JS_SetPropertyStr(ctx, obj, "free", JS_NewInt64(ctx, (int64_t)(vfs.f_bfree * bs))); JS_SetPropertyStr(ctx, obj, "available", JS_NewInt64(ctx, (int64_t)(vfs.f_bavail * bs))); JS_SetPropertyStr(ctx, obj, "blockSize", JS_NewInt64(ctx, (int64_t)bs)); return obj;}
// system.blockDevices() — list /sys/block entries with size + removable + model.// Returns array of {name, sizeBytes, removable, model, vendor} or null.static JSValue js_block_devices(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; DIR *dir = opendir("/sys/block"); if (!dir) return JS_NULL; JSValue arr = JS_NewArray(ctx); struct dirent *ent; int idx = 0; while ((ent = readdir(dir)) != NULL) { if (ent->d_name[0] == '.') continue; // Skip loop/ram/dm pseudo devs if (strncmp(ent->d_name, "loop", 4) == 0) continue; if (strncmp(ent->d_name, "ram", 3) == 0) continue; if (strncmp(ent->d_name, "dm-", 3) == 0) continue; char p[256]; char buf[128]; JSValue obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, ent->d_name)); // size (in 512-byte sectors) snprintf(p, sizeof(p), "/sys/block/%s/size", ent->d_name); FILE *f = fopen(p, "r"); long long sectors = 0; if (f) { fscanf(f, "%lld", §ors); fclose(f); } JS_SetPropertyStr(ctx, obj, "sizeBytes", JS_NewInt64(ctx, sectors * 512LL)); // removable flag snprintf(p, sizeof(p), "/sys/block/%s/removable", ent->d_name); int removable = 0; f = fopen(p, "r"); if (f) { fscanf(f, "%d", &removable); fclose(f); } JS_SetPropertyStr(ctx, obj, "removable", JS_NewBool(ctx, removable != 0)); // vendor + model const char *fields[][2] = { {"vendor", "vendor"}, {"model", "model"}, {NULL, NULL} }; for (int i = 0; fields[i][0]; i++) { snprintf(p, sizeof(p), "/sys/block/%s/device/%s", ent->d_name, fields[i][0]); f = fopen(p, "r"); if (f) { if (fgets(buf, sizeof(buf), f)) { // trim trailing whitespace size_t L = strlen(buf); while (L > 0 && (buf[L-1] == '\n' || buf[L-1] == ' ' || buf[L-1] == '\t')) buf[--L] = 0; JS_SetPropertyStr(ctx, obj, fields[i][1], JS_NewString(ctx, buf)); } fclose(f); } } JS_SetPropertyUint32(ctx, arr, idx++, obj); } closedir(dir); return arr;}
// system.listDir(path) — returns [{name, isDir, size}, ...] or nullstatic JSValue js_list_dir(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_NULL; const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_NULL;
DIR *dir = opendir(path); if (!dir) { JS_FreeCString(ctx, path); return JS_NULL; }
JSValue arr = JS_NewArray(ctx); struct dirent *ent; int idx = 0; while ((ent = readdir(dir)) != NULL) { // Skip . and .. if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) continue;
JSValue obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, ent->d_name));
// stat for size and type char full[1024]; snprintf(full, sizeof(full), "%s/%s", path, ent->d_name); struct stat st; int is_dir = 0; int64_t size = 0; if (stat(full, &st) == 0) { is_dir = S_ISDIR(st.st_mode); size = st.st_size; } JS_SetPropertyStr(ctx, obj, "isDir", JS_NewBool(ctx, is_dir)); JS_SetPropertyStr(ctx, obj, "size", JS_NewInt64(ctx, size));
JS_SetPropertyUint32(ctx, arr, idx++, obj); } closedir(dir); JS_FreeCString(ctx, path); return arr;}
// system.mountMusic() — non-blocking secondary USB probe.// Schedules a background mount check for /media and immediately returns the// last known mounted state. JS reads system.mountMusicMounted /// system.mountMusicPending for cached state.static pthread_mutex_t music_mount_mu = PTHREAD_MUTEX_INITIALIZER;static volatile int music_mount_pending = 0;static volatile int music_mount_state = 0;static int music_mount_last_result = -1;static char music_mount_last_skip_base[64] = "";
static int probe_mount_music_once(void) {
mkdir("/media", 0755);
// Check /proc/mounts for any existing mount at /media, and capture the device. char media_dev[128] = {0}; FILE *mf = fopen("/proc/mounts", "r"); if (mf) { char line[512]; while (fgets(line, sizeof(line), mf)) { char dev[128], target[128]; if (sscanf(line, "%127s %127s", dev, target) == 2 && strcmp(target, "/media") == 0) { strncpy(media_dev, dev, sizeof(media_dev) - 1); break; } } fclose(mf); }
if (media_dev[0]) { // There's something mounted at /media. Verify the backing device is still // physically present (USB stick didn't get yanked). struct stat ds; int dev_alive = (stat(media_dev, &ds) == 0 && S_ISBLK(ds.st_mode)); // Extra check: opendir succeeds on a live mount but may EIO on a stale one. int dir_ok = 0; if (dev_alive) { DIR *d = opendir("/media"); if (d) { dir_ok = 1; closedir(d); } } if (dev_alive && dir_ok) { music_mount_last_result = 1; return 1; // legitimate, live mount } // Stale: device is gone or dir unreadable. Lazy-unmount so we can remount. ac_log("[dj] stale /media mount (dev=%s, alive=%d, dir=%d) — detaching\n", media_dev, dev_alive, dir_ok); if (umount2("/media", MNT_DETACH) != 0) { ac_log("[dj] umount /media failed: %s\n", strerror(errno)); } }
// Find ALL mounted devices to skip (boot USB may have multiple partitions) char skip_bases[8][64]; int skip_count = 0; FILE *fp = fopen("/proc/mounts", "r"); if (fp) { char line[256]; while (fgets(line, sizeof(line), fp) && skip_count < 8) { char mdev[64]; if (sscanf(line, "%63s", mdev) == 1 && strncmp(mdev, "/dev/sd", 7) == 0) { // Strip partition number to get base char base[64]; snprintf(base, sizeof(base), "%s", mdev); int len = strlen(base); while (len > 0 && base[len-1] >= '0' && base[len-1] <= '9') base[--len] = '\0'; // Add if not already in skip list int found = 0; for (int i = 0; i < skip_count; i++) if (strcmp(skip_bases[i], base) == 0) { found = 1; break; } if (!found) { strncpy(skip_bases[skip_count++], base, 63); if (strcmp(music_mount_last_skip_base, base) != 0) { snprintf(music_mount_last_skip_base, sizeof(music_mount_last_skip_base), "%s", base); ac_log("[dj] skip boot device: %s\n", base); } } } } fclose(fp); }
// Try mounting partitions on non-boot USB devices const char *bases[] = { "/dev/sda", "/dev/sdb", "/dev/sdc", "/dev/sdd", NULL }; for (int b = 0; bases[b]; b++) { int skip = 0; for (int i = 0; i < skip_count; i++) if (strcmp(bases[b], skip_bases[i]) == 0) { skip = 1; break; } if (skip) continue;
for (int p = 1; p <= 8; p++) { char dev[64]; snprintf(dev, sizeof(dev), "%s%d", bases[b], p); struct stat ds; if (stat(dev, &ds) != 0) continue;
if (mount(dev, "/media", "vfat", MS_RDONLY, "iocharset=utf8") == 0) { ac_log("[dj] mounted %s at /media (vfat)\n", dev); music_mount_last_result = 1; return 1; } if (mount(dev, "/media", "exfat", MS_RDONLY, NULL) == 0) { ac_log("[dj] mounted %s at /media (exfat)\n", dev); music_mount_last_result = 1; return 1; } if (mount(dev, "/media", "ext4", MS_RDONLY, NULL) == 0) { ac_log("[dj] mounted %s at /media (ext4)\n", dev); music_mount_last_result = 1; return 1; } if (mount(dev, "/media", "ntfs3", MS_RDONLY, NULL) == 0) { ac_log("[dj] mounted %s at /media (ntfs)\n", dev); music_mount_last_result = 1; return 1; } } } if (music_mount_last_result != 0) { ac_log("[dj] no music USB found\n"); music_mount_last_result = 0; } return 0;}
static void *music_mount_thread_fn(void *arg) { (void)arg; int mounted = probe_mount_music_once(); pthread_mutex_lock(&music_mount_mu); music_mount_state = mounted ? 1 : 0; music_mount_pending = 0; pthread_mutex_unlock(&music_mount_mu); return NULL;}
static void request_music_mount_probe(void) { int should_spawn = 0; pthread_t tid;
pthread_mutex_lock(&music_mount_mu); if (!music_mount_pending) { music_mount_pending = 1; should_spawn = 1; } pthread_mutex_unlock(&music_mount_mu);
if (!should_spawn) return;
if (pthread_create(&tid, NULL, music_mount_thread_fn, NULL) != 0) { pthread_mutex_lock(&music_mount_mu); music_mount_pending = 0; pthread_mutex_unlock(&music_mount_mu); ac_log("[dj] mountMusic thread create failed\n"); return; } pthread_detach(tid);}
static JSValue js_mount_music(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; request_music_mount_probe(); return JS_NewBool(ctx, music_mount_state);}
// ---------------------------------------------------------------------------// system.ws — WebSocket client// ---------------------------------------------------------------------------
static JSValue js_ws_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt) return JS_UNDEFINED; const char *url = JS_ToCString(ctx, argv[0]); if (!url) return JS_UNDEFINED; ws_connect(current_rt->ws, url); // non-blocking: signals background thread JS_FreeCString(ctx, url); return JS_UNDEFINED;}
static JSValue js_ws_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt || !current_rt->ws) return JS_UNDEFINED; const char *text = JS_ToCString(ctx, argv[0]); if (!text) return JS_UNDEFINED; ws_send(current_rt->ws, text); JS_FreeCString(ctx, text); return JS_UNDEFINED;}
static JSValue js_ws_close(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (current_rt && current_rt->ws) ws_close(current_rt->ws); return JS_UNDEFINED;}
static JSValue build_ws_obj(JSContext *ctx, const char *phase) { JSValue ws_obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, ws_obj, "connect", JS_NewCFunction(ctx, js_ws_connect, "connect", 1)); JS_SetPropertyStr(ctx, ws_obj, "send", JS_NewCFunction(ctx, js_ws_send, "send", 1)); JS_SetPropertyStr(ctx, ws_obj, "close", JS_NewCFunction(ctx, js_ws_close, "close", 0));
ACWs *ws = current_rt ? current_rt->ws : NULL; if (ws) { // Only drain messages during "paint" phase — that's where JS processes them. // Other phases (act, sim) see connected/connecting but empty messages array, // preventing the queue from being consumed before paint() can read it. int drain = (strcmp(phase, "paint") == 0); pthread_mutex_lock(&ws->mu); int count = drain ? ws->msg_count : 0; if (drain) ws->msg_count = 0; int connected = ws->connected; int connecting = ws->connecting; // Copy only actual message bytes (not full 256KB buffer each) — heap alloc if (count > WS_MAX_MESSAGES) count = WS_MAX_MESSAGES; char *msgs[WS_MAX_MESSAGES]; int msg_lens[WS_MAX_MESSAGES]; for (int i = 0; i < count; i++) { int len = strnlen(ws->messages[i], WS_MAX_MSG_LEN - 1); msgs[i] = malloc(len + 1); if (msgs[i]) { memcpy(msgs[i], ws->messages[i], len); msgs[i][len] = 0; } msg_lens[i] = len; } pthread_mutex_unlock(&ws->mu);
JS_SetPropertyStr(ctx, ws_obj, "connected", JS_NewBool(ctx, connected)); JS_SetPropertyStr(ctx, ws_obj, "connecting", JS_NewBool(ctx, connecting));
JSValue arr = JS_NewArray(ctx); for (int i = 0; i < count; i++) { if (msgs[i]) { JS_SetPropertyUint32(ctx, arr, (uint32_t)i, JS_NewStringLen(ctx, msgs[i], msg_lens[i])); free(msgs[i]); } } JS_SetPropertyStr(ctx, ws_obj, "messages", arr); } else { JS_SetPropertyStr(ctx, ws_obj, "connected", JS_FALSE); JS_SetPropertyStr(ctx, ws_obj, "connecting", JS_FALSE); JS_SetPropertyStr(ctx, ws_obj, "messages", JS_NewArray(ctx)); } return ws_obj;}
// system.fetch(url) — async HTTP GET via curl, result polled via system.fetchResultstatic JSValue js_fetch(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 1) return JS_UNDEFINED; // Reject if a fetch is already in flight (single-slot) if (current_rt->fetch_pending) return JS_FALSE; const char *url = JS_ToCString(ctx, argv[0]); if (!url) return JS_UNDEFINED; unlink("/tmp/ac_fetch.json"); unlink("/tmp/ac_fetch_rc"); unlink("/tmp/ac_fetch_err"); char cmd[2048]; ac_log("[fetch] start: %s\n", url); snprintf(cmd, sizeof(cmd), "sh -c 'curl -fsSL --retry 2 --retry-delay 1 --connect-timeout 5 --max-time 12 " "--cacert /etc/pki/tls/certs/ca-bundle.crt " "--output /tmp/ac_fetch.json \"%s\" 2>/tmp/ac_fetch_err;" " echo $? > /tmp/ac_fetch_rc' &", url); system(cmd); current_rt->fetch_pending = 1; current_rt->fetch_result[0] = 0; current_rt->fetch_error[0] = 0; JS_FreeCString(ctx, url); return JS_TRUE;}
// system.fetchPost(url, body, headersJSON) — POST request with custom headers// Uses the same single fetch slot as system.fetch.// headersJSON is a JSON string like '{"Authorization":"Bearer ...","Content-Type":"application/json"}'static JSValue js_fetch_post(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 2) return JS_UNDEFINED; if (current_rt->fetch_pending) return JS_FALSE; const char *url = JS_ToCString(ctx, argv[0]); const char *body = JS_ToCString(ctx, argv[1]); const char *headers_json = (argc >= 3) ? JS_ToCString(ctx, argv[2]) : NULL; if (!url || !body) { JS_FreeCString(ctx, url); JS_FreeCString(ctx, body); if (headers_json) JS_FreeCString(ctx, headers_json); return JS_UNDEFINED; } unlink("/tmp/ac_fetch.json"); unlink("/tmp/ac_fetch_rc"); unlink("/tmp/ac_fetch_err");
// Write body to temp file to avoid shell escaping issues FILE *bf = fopen("/tmp/ac_fetch_body.json", "w"); if (bf) { fputs(body, bf); fclose(bf); }
// Build header flags by parsing simple JSON {"key":"value",...} // We build a file of -H flags to avoid shell escaping nightmares FILE *hf = fopen("/tmp/ac_fetch_headers.txt", "w"); if (hf) { // Always include Content-Type fprintf(hf, "-H\nContent-Type: application/json\n"); if (headers_json) { // Simple JSON parser: find "key":"value" pairs const char *p = headers_json; while (*p) { const char *kq = strchr(p, '"'); if (!kq) break; const char *ke = strchr(kq + 1, '"'); if (!ke) break; const char *vq = strchr(ke + 1, '"'); if (!vq) break; const char *ve = strchr(vq + 1, '"'); if (!ve) break; int klen = (int)(ke - kq - 1); int vlen = (int)(ve - vq - 1); if (klen > 0 && klen < 256 && vlen > 0 && vlen < 2048) { fprintf(hf, "-H\n%.*s: %.*s\n", klen, kq + 1, vlen, vq + 1); } p = ve + 1; } } fclose(hf); }
ac_log("[fetchPost] start: %s (body %ld bytes)\n", url, (long)strlen(body)); char cmd[2048]; snprintf(cmd, sizeof(cmd), "sh -c 'curl -fsSL -X POST --retry 1 --connect-timeout 10 --max-time 120 " "--cacert /etc/pki/tls/certs/ca-bundle.crt " "-K /tmp/ac_fetch_headers.txt " "-d @/tmp/ac_fetch_body.json " "--output /tmp/ac_fetch.json \"%s\" 2>/tmp/ac_fetch_err;" " echo $? > /tmp/ac_fetch_rc' &", url); system(cmd); current_rt->fetch_pending = 1; current_rt->fetch_result[0] = 0; current_rt->fetch_error[0] = 0; JS_FreeCString(ctx, url); JS_FreeCString(ctx, body); if (headers_json) JS_FreeCString(ctx, headers_json); return JS_TRUE;}
// system.fetchCancel() — kill in-flight curl and free the fetch slotstatic JSValue js_fetch_cancel(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt) return JS_UNDEFINED; if (current_rt->fetch_pending) { ac_log("[fetch] cancel: killing in-flight curl\n"); // Kill any background curl for ac_fetch system("pkill -f 'curl.*ac_fetch' 2>/dev/null"); unlink("/tmp/ac_fetch.json"); unlink("/tmp/ac_fetch_rc"); unlink("/tmp/ac_fetch_err"); current_rt->fetch_pending = 0; current_rt->fetch_result[0] = 0; current_rt->fetch_error[0] = 0; } return JS_UNDEFINED;}
// ---------------------------------------------------------------------------// system.scanQR() — Open camera and scan for QR codes in a background thread// system.scanQRStop() — Stop scanning and close camera// Poll system.qrPending / system.qrResult each frame.// ---------------------------------------------------------------------------
static void *qr_scan_thread(void *arg) { ACRuntime *rt = (ACRuntime *)arg; ACCamera *cam = &rt->camera;
if (camera_open(cam) < 0) { cam->scan_done = 1; rt->qr_thread_running = 0; return NULL; }
// Grab frames and scan until we find a QR code or are told to stop int max_attempts = 300; // ~10 seconds at 30fps for (int i = 0; i < max_attempts && rt->qr_scan_active; i++) { if (camera_grab(cam) == 0) { if (camera_scan_qr(cam) > 0) { cam->scan_done = 1; break; } } usleep(33000); // ~30fps }
if (!cam->scan_done) { cam->scan_done = 1; if (!cam->scan_result[0] && !cam->scan_error[0]) { snprintf(cam->scan_error, sizeof(cam->scan_error), "no QR code found"); } }
camera_close(cam); rt->qr_scan_active = 0; rt->qr_thread_running = 0; return NULL;}
static JSValue js_scan_qr(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt) return JS_UNDEFINED;
// Already scanning? if (current_rt->qr_scan_active) return JS_FALSE;
// Reset state current_rt->camera.scan_result[0] = 0; current_rt->camera.scan_error[0] = 0; current_rt->camera.scan_done = 0; current_rt->qr_scan_active = 1; current_rt->qr_thread_running = 1;
if (pthread_create(¤t_rt->qr_thread, NULL, qr_scan_thread, current_rt) != 0) { current_rt->qr_scan_active = 0; current_rt->qr_thread_running = 0; snprintf(current_rt->camera.scan_error, sizeof(current_rt->camera.scan_error), "thread create failed"); current_rt->camera.scan_done = 1; return JS_FALSE; } pthread_detach(current_rt->qr_thread);
ac_log("[qr] scan started\n"); return JS_TRUE;}
static JSValue js_scan_qr_stop(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; (void)ctx; if (!current_rt) return JS_UNDEFINED; current_rt->qr_scan_active = 0; ac_log("[qr] scan stopped\n"); return JS_UNDEFINED;}
// cameraBlit(x, y, w, h, mirror, flip) — render camera display buffer to// graph framebuffer. Prefers the ARGB color frame and falls back to// grayscale. mirror=1 flips horizontally (selfie preview); flip=1 rotates// 180° — for a bent-back screen used as a performer-facing monitor, and// for cameras that are physically upside down in that position. Both// affect the recorded tape too (the recorder captures the screen), which// is the point: footage matches what the videographer saw.static JSValue js_camera_blit(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->graph) return JS_FALSE; ACCamera *cam = ¤t_rt->camera; if (!cam->display_ready && !cam->display_rgb_ready) return JS_FALSE;
int dx = 0, dy = 0, dw = 0, dh = 0, mirror = 0, flip = 0; if (argc >= 4) { JS_ToInt32(ctx, &dx, argv[0]); JS_ToInt32(ctx, &dy, argv[1]); JS_ToInt32(ctx, &dw, argv[2]); JS_ToInt32(ctx, &dh, argv[3]); } else { // Default: fit to screen dw = current_rt->graph->fb->width; dh = current_rt->graph->fb->height; } if (argc >= 5) JS_ToInt32(ctx, &mirror, argv[4]); if (argc >= 6) JS_ToInt32(ctx, &flip, argv[5]); if (dw <= 0 || dh <= 0) return JS_FALSE;
// Lock and copy whichever display buffer is freshest to a local copy. // Pointer checks re-run under the lock — camera_close frees these // buffers while holding display_mu. int cw = cam->width, ch = cam->height; int pixels = cw * ch; int color = 0; uint32_t *local_rgb = NULL; uint8_t *local_gray = NULL;
pthread_mutex_lock(&cam->display_mu); if (cam->display_rgb && cam->display_rgb_ready) { local_rgb = malloc((size_t)pixels * sizeof(uint32_t)); if (local_rgb) { memcpy(local_rgb, cam->display_rgb, (size_t)pixels * sizeof(uint32_t)); color = 1; } } if (!color && cam->display && cam->display_ready) { local_gray = malloc(pixels); if (local_gray) memcpy(local_gray, cam->display, pixels); } pthread_mutex_unlock(&cam->display_mu); if (!color && !local_gray) return JS_FALSE;
// Blit to framebuffer with nearest-neighbor scaling. flip rotates // 180° (both axes); mirror flips horizontally on top of that. ACFramebuffer *fb = current_rt->graph->fb; for (int py = 0; py < dh; py++) { int fy = dy + py; if (fy < 0 || fy >= fb->height) continue; int syp = flip ? dh - 1 - py : py; int sy = syp * ch / dh; if (sy >= ch) sy = ch - 1; for (int px = 0; px < dw; px++) { int fx = dx + px; if (fx < 0 || fx >= fb->width) continue; int sxp = flip ? dw - 1 - px : px; if (mirror) sxp = dw - 1 - sxp; int sx = sxp * cw / dw; if (sx >= cw) sx = cw - 1; if (color) { fb->pixels[fy * fb->stride + fx] = local_rgb[sy * cw + sx]; } else { uint8_t g = local_gray[sy * cw + sx]; fb->pixels[fy * fb->stride + fx] = 0xFF000000u | ((uint32_t)g << 16) | ((uint32_t)g << 8) | g; } } }
free(local_rgb); free(local_gray); return JS_TRUE;}
// ---------------------------------------------------------------------------// Continuous camera streaming — system.cameraStart() / system.cameraStop()// Powers the 'cap' piece: opens the webcam and grabs frames (~30fps) into// the shared display buffers until stopped. Mutually exclusive with QR// scanning, which owns the same ACCamera.// ---------------------------------------------------------------------------
static void *cam_stream_thread_fn(void *arg) { ACRuntime *rt = (ACRuntime *)arg; ACCamera *cam = &rt->camera; int announced_wait = 0; char devices[10][16]; char prev[10][16]; int prev_count = -1; // -1 = first poll (skip the auto-switch compare) char open_path[16] = {0};
// Plug-and-play loop: re-enumerate /dev/videoN roughly once a second // between grab bursts. Built-in and USB cams both count; a freshly // plugged USB cam auto-takes the stream ("switch out cams per shot"), // and cam_switch_req (Tab in cap) cycles through everything present. while (rt->cam_stream_active) { int count = camera_list(devices, 10); rt->cam_device_count = count;
if (count == 0) { if (open_path[0]) { camera_close(cam); open_path[0] = 0; } if (!announced_wait) { ac_log("[camera] no camera yet — waiting for hotplug\n"); announced_wait = 1; } prev_count = 0; for (int i = 0; i < 10 && rt->cam_stream_active; i++) usleep(100000); // rescan ~1s continue; } announced_wait = 0;
// Target: keep the open device if it's still present, else the // highest-numbered one (USB enumerates above the built-in). int cur_idx = -1; for (int i = 0; i < count; i++) if (open_path[0] && strcmp(devices[i], open_path) == 0) cur_idx = i; char target[16]; snprintf(target, sizeof(target), "%s", cur_idx >= 0 ? open_path : devices[count - 1]);
// A device that wasn't in the previous poll was just plugged in — // hand it the stream. if (prev_count >= 0) { for (int i = 0; i < count; i++) { int seen = 0; for (int j = 0; j < prev_count; j++) if (strcmp(devices[i], prev[j]) == 0) { seen = 1; break; } if (!seen) { ac_log("[camera] %s plugged in — switching\n", devices[i]); snprintf(target, sizeof(target), "%s", devices[i]); } } } memcpy(prev, devices, sizeof(prev)); prev_count = count;
// Tab: cycle to the next camera in enumeration order. if (rt->cam_switch_req) { rt->cam_switch_req = 0; if (cur_idx >= 0 && count > 1) { snprintf(target, sizeof(target), "%s", devices[(cur_idx + 1) % count]); ac_log("[camera] switch → %s\n", target); } }
if (!open_path[0] || strcmp(target, open_path) != 0) { if (open_path[0]) camera_close(cam); if (camera_open_path(cam, target) < 0) { ac_log("[camera] open %s failed (%s)\n", target, cam->scan_error); open_path[0] = 0; for (int i = 0; i < 5 && rt->cam_stream_active; i++) usleep(100000); continue; } snprintf(open_path, sizeof(open_path), "%s", target); }
// Grab ~1s of frames, then loop back to re-poll (hotplug + Tab). for (int f = 0; f < 30 && rt->cam_stream_active && !rt->cam_switch_req; f++) { int r = camera_grab(cam); if (r == -2) { ac_log("[camera] device lost (%s) — rescanning\n", cam->scan_error); camera_close(cam); open_path[0] = 0; break; } usleep(33000); // ~30fps } }
if (open_path[0]) camera_close(cam); rt->cam_device_count = 0; rt->cam_stream_running = 0; return NULL;}
static JSValue js_camera_start(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; (void)ctx; if (!current_rt) return JS_FALSE; // Already streaming, or QR scan owns the camera if (current_rt->cam_stream_active || current_rt->cam_stream_running) return JS_TRUE; if (current_rt->qr_scan_active) return JS_FALSE;
current_rt->camera.scan_error[0] = 0; current_rt->cam_switch_req = 0; current_rt->cam_device_count = 0; current_rt->cam_stream_active = 1; current_rt->cam_stream_running = 1; if (pthread_create(¤t_rt->cam_stream_thread, NULL, cam_stream_thread_fn, current_rt) != 0) { current_rt->cam_stream_active = 0; current_rt->cam_stream_running = 0; return JS_FALSE; } pthread_detach(current_rt->cam_stream_thread); ac_log("[camera] stream started\n"); return JS_TRUE;}
static JSValue js_camera_stop(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; (void)ctx; if (!current_rt) return JS_UNDEFINED; current_rt->cam_stream_active = 0; ac_log("[camera] stream stopped\n"); return JS_UNDEFINED;}
// system.cameraReady() -> bool — true once a display frame has landedstatic JSValue js_camera_ready(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt) return JS_FALSE; return JS_NewBool(ctx, (current_rt->camera.display_ready || current_rt->camera.display_rgb_ready) ? 1 : 0);}
// system.cameraError() -> string ("" while healthy/opening)static JSValue js_camera_error(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt) return JS_NewString(ctx, ""); // Only report once the stream thread has given up if (current_rt->cam_stream_active || current_rt->cam_stream_running) return JS_NewString(ctx, ""); return JS_NewString(ctx, current_rt->camera.scan_error);}
// system.cameraSwitch() — cycle to the next available camera (Tab in cap).// The stream thread consumes the request on its next poll.static JSValue js_camera_switch(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; (void)ctx; if (!current_rt || !current_rt->cam_stream_active) return JS_FALSE; current_rt->cam_switch_req = 1; return JS_TRUE;}
// system.cameraCount() -> number of capture devices from the last pollstatic JSValue js_camera_count(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; return JS_NewInt32(ctx, current_rt ? current_rt->cam_device_count : 0);}
// ---------------------------------------------------------------------------// Tape control — system.tapeStart() / system.tapeStop()// Thin wrappers over ac_tape_start/stop in ac-native.c (same code path as// the PrintScreen key toggle: MP4 to /mnt/tapes + background cloud upload).// Called quiet so recordings don't open with the "tape rolling" announce.// ---------------------------------------------------------------------------
extern int ac_tape_start(int quiet);extern int ac_tape_stop(int quiet);
static JSValue js_tape_start(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; return JS_NewBool(ctx, ac_tape_start(1) == 0);}
static JSValue js_tape_stop(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; return JS_NewBool(ctx, ac_tape_stop(1) == 0);}
// ---------------------------------------------------------------------------// system.udp — Raw UDP fairy point co-presence// ---------------------------------------------------------------------------
static void sync_udp_identity(void) { extern char g_machine_id[64];
if (!current_rt || !current_rt->udp) return; udp_set_identity( current_rt->udp, current_rt->handle[0] ? current_rt->handle : "", g_machine_id[0] ? g_machine_id : "unknown" );}
static JSValue js_udp_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->udp) return JS_UNDEFINED; const char *host = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; if (!host) host = "session-server.aesthetic.computer"; int port = UDP_FAIRY_PORT; if (argc > 1) JS_ToInt32(ctx, &port, argv[1]); sync_udp_identity(); udp_connect(current_rt->udp, host, port); if (argc > 0) JS_FreeCString(ctx, host); return JS_UNDEFINED;}
static JSValue js_udp_send_fairy(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->udp || argc < 2) return JS_UNDEFINED; double x, y; JS_ToFloat64(ctx, &x, argv[0]); JS_ToFloat64(ctx, &y, argv[1]); udp_send_fairy(current_rt->udp, (float)x, (float)y); return JS_UNDEFINED;}
static JSValue js_udp_send_midi(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->udp || argc < 4) return JS_UNDEFINED;
const char *event = JS_ToCString(ctx, argv[0]); int32_t note = 0; int32_t velocity = 0; int32_t channel = 0; const char *piece = argc > 4 ? JS_ToCString(ctx, argv[4]) : NULL;
JS_ToInt32(ctx, ¬e, argv[1]); JS_ToInt32(ctx, &velocity, argv[2]); JS_ToInt32(ctx, &channel, argv[3]);
if (!event) { if (piece) JS_FreeCString(ctx, piece); return JS_UNDEFINED; }
sync_udp_identity(); udp_send_midi( current_rt->udp, event, (int)note, (int)velocity, (int)channel, piece ? piece : "notepat" );
JS_FreeCString(ctx, event); if (piece) JS_FreeCString(ctx, piece); return JS_UNDEFINED;}
static JSValue js_udp_send_midi_heartbeat(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->udp) return JS_UNDEFINED;
const char *piece = argc > 0 ? JS_ToCString(ctx, argv[0]) : NULL; sync_udp_identity(); udp_send_midi_heartbeat(current_rt->udp, piece ? piece : "notepat"); if (piece) JS_FreeCString(ctx, piece); return JS_UNDEFINED;}
static JSValue build_udp_obj(JSContext *ctx, const char *phase) { JSValue obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, obj, "connect", JS_NewCFunction(ctx, js_udp_connect, "connect", 2)); JS_SetPropertyStr(ctx, obj, "sendFairy", JS_NewCFunction(ctx, js_udp_send_fairy, "sendFairy", 2)); JS_SetPropertyStr(ctx, obj, "sendMidi", JS_NewCFunction(ctx, js_udp_send_midi, "sendMidi", 5)); JS_SetPropertyStr(ctx, obj, "sendMidiHeartbeat", JS_NewCFunction(ctx, js_udp_send_midi_heartbeat, "sendMidiHeartbeat", 1));
ACUdp *udp = current_rt ? current_rt->udp : NULL; if (udp) { sync_udp_identity(); JS_SetPropertyStr(ctx, obj, "connected", JS_NewBool(ctx, udp->connected)); JS_SetPropertyStr(ctx, obj, "handle", JS_NewString(ctx, current_rt && current_rt->handle[0] ? current_rt->handle : ""));
// Only deliver fairies during paint phase if (strcmp(phase, "paint") == 0) { UDPFairy fairies[UDP_MAX_FAIRIES]; int count = udp_poll_fairies(udp, fairies, UDP_MAX_FAIRIES); JSValue arr = JS_NewArray(ctx); for (int i = 0; i < count; i++) { JSValue f = JS_NewObject(ctx); JS_SetPropertyStr(ctx, f, "x", JS_NewFloat64(ctx, fairies[i].x)); JS_SetPropertyStr(ctx, f, "y", JS_NewFloat64(ctx, fairies[i].y)); JS_SetPropertyUint32(ctx, arr, i, f); } JS_SetPropertyStr(ctx, obj, "fairies", arr); } } return obj;}
// system.fetchBinary(url, destPath[, expectedBytes])// Non-blocking: runs curl in background; poll system.fetchBinaryProgress/Done/Ok each frame.static JSValue js_fetch_binary(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 2) return JS_UNDEFINED; const char *url = JS_ToCString(ctx, argv[0]); const char *dest = JS_ToCString(ctx, argv[1]); if (!url || !dest) { JS_FreeCString(ctx, url); JS_FreeCString(ctx, dest); return JS_UNDEFINED; } long expected = 0; if (argc >= 3) { double v; JS_ToFloat64(ctx, &v, argv[2]); expected = (long)v; } ac_log("[fetchBinary] start: url=%s dest=%s expected=%ld\n", url, dest, expected); // Reset state current_rt->fetch_binary_pending = 1; current_rt->fetch_binary_done = 0; current_rt->fetch_binary_ok = 0; current_rt->fetch_binary_progress = 0.0f; current_rt->fetch_binary_expected = expected; strncpy(current_rt->fetch_binary_dest, dest, sizeof(current_rt->fetch_binary_dest) - 1); current_rt->fetch_binary_dest[sizeof(current_rt->fetch_binary_dest) - 1] = 0; // Remove stale files unlink("/tmp/ac_fb_rc"); unlink("/tmp/ac_fb_err"); unlink(dest); // Start curl in background. Budget is generous: 326 MB initramfs over // a slow link (~200 KB/s) takes ~27 min, so --max-time 1800 (30 min) // covers it. --retry 5 + backoff absorbs transient TLS/DNS hiccups. // --connect-timeout 15 tolerates sleepy Wi-Fi handshakes. char cmd[1024]; snprintf(cmd, sizeof(cmd), "sh -c 'curl -fSL --retry 5 --retry-delay 2 --retry-all-errors " "--connect-timeout 15 --max-time 1800 " "--cacert /etc/pki/tls/certs/ca-bundle.crt " "--output \"%s\" \"%s\" 2>/tmp/ac_fb_err;" " echo $? > /tmp/ac_fb_rc' &", dest, url); ac_log("[fetchBinary] cmd: curl -> %s\n", dest); system(cmd); JS_FreeCString(ctx, url); JS_FreeCString(ctx, dest); return JS_UNDEFINED;}
// Detect the EFI partition we booted from:// - If /sys/block/sda/removable == 1 → USB → /dev/sda1// - Else if /dev/nvme0n1p1 exists → NVMe internal → /dev/nvme0n1p1// - Else if /dev/mmcblk0p1 exists → eMMC internal (Chromebooks) → /dev/mmcblk0p1// - Fallback: /dev/sda1static void detect_boot_device(char *out, size_t len) { char removable[8] = {0}; if (read_sysfs("/sys/block/sda/removable", removable, sizeof(removable)) > 0 && removable[0] == '1') { snprintf(out, len, "/dev/sda1"); ac_log("[flash] boot device: USB (/dev/sda1, removable)"); return; } if (access("/dev/nvme0n1p1", F_OK) == 0) { snprintf(out, len, "/dev/nvme0n1p1"); ac_log("[flash] boot device: NVMe (/dev/nvme0n1p1)"); return; } if (access("/dev/mmcblk0p1", F_OK) == 0) { snprintf(out, len, "/dev/mmcblk0p1"); ac_log("[flash] boot device: eMMC (/dev/mmcblk0p1)"); return; } // Fallback — try sda1 anyway snprintf(out, len, "/dev/sda1"); ac_log("[flash] boot device: fallback (/dev/sda1)");}
// Pure C file copy (no shell needed)static long flash_copy_file(const char *src, const char *dst) { FILE *in = fopen(src, "rb"); if (!in) { ac_log("[flash] fopen src failed: %s errno=%d (%s)", src, errno, strerror(errno)); return -1; } // Delete destination first, then sync to flush vfat metadata unlink(dst); sync(); FILE *out = fopen(dst, "wb"); if (!out) { ac_log("[flash] fopen dst failed: %s errno=%d (%s)", dst, errno, strerror(errno)); fclose(in); return -1; } char buf[65536]; long total = 0; size_t n; while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { if (fwrite(buf, 1, n, out) != n) { ac_log("[flash] fwrite failed at offset %ld errno=%d (%s)", total, errno, strerror(errno)); total = -1; break; } total += n; } // Flush stdio buffer, then fsync to force data+metadata to physical disk // (critical for vfat — without this, data stays in page cache and reboot loses it) if (total > 0) { fflush(out); if (fsync(fileno(out)) != 0) { ac_log("[flash] fsync failed: errno=%d (%s)", errno, strerror(errno)); total = -1; } } fclose(out); fclose(in); return total;}
// Byte-for-byte verification: evict destination page cache, re-read from disk,// compare against source (which lives in tmpfs and must NOT be evicted).// Returns number of verified bytes, or -1 on mismatch/error.static void flash_trace(const char *fmt, ...);static void flash_trace_open(void);static void flash_trace_close_and_archive(void);static long flash_verify(const char *src, const char *dst) { // SIZE CHECK FIRST — cheap, diagnostic, catches the common failure // where the copy was short (disk full, sync incomplete, device removed // mid-write, etc.) without needing to read 272 MB twice. struct stat src_st, dst_st; if (stat(src, &src_st) != 0) { ac_log("[verify] stat src=%s failed errno=%d", src, errno); flash_trace("verify: stat src FAILED errno=%d", errno); return -1; } if (stat(dst, &dst_st) != 0) { ac_log("[verify] stat dst=%s failed errno=%d", dst, errno); flash_trace("verify: stat dst FAILED errno=%d", errno); return -1; } flash_trace("verify: src=%ld dst=%ld", (long)src_st.st_size, (long)dst_st.st_size); if (src_st.st_size != dst_st.st_size) { ac_log("[verify] SIZE MISMATCH: src=%ld dst=%ld (diff=%ld)", (long)src_st.st_size, (long)dst_st.st_size, (long)(src_st.st_size - dst_st.st_size)); flash_trace("verify: SIZE MISMATCH src=%ld dst=%ld", (long)src_st.st_size, (long)dst_st.st_size); return -1; }
// Evict ONLY the destination file's page cache using posix_fadvise. // DO NOT use drop_caches — that nukes tmpfs pages and destroys the source! int dst_fd = open(dst, O_RDONLY); if (dst_fd >= 0) { posix_fadvise(dst_fd, 0, dst_st.st_size, POSIX_FADV_DONTNEED); ac_log("[verify] evicted dst page cache (%ld bytes)", (long)dst_st.st_size); flash_trace("verify: evicted page cache for dst"); close(dst_fd); }
FILE *fa = fopen(src, "rb"); FILE *fb = fopen(dst, "rb"); if (!fa || !fb) { ac_log("[verify] fopen failed: src=%s dst=%s", fa ? "ok" : "FAIL", fb ? "ok" : "FAIL"); flash_trace("verify: fopen src=%s dst=%s", fa ? "ok" : "FAIL", fb ? "ok" : "FAIL"); if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
char bufa[65536], bufb[65536]; long verified = 0; size_t na, nb; while ((na = fread(bufa, 1, sizeof(bufa), fa)) > 0) { nb = fread(bufb, 1, sizeof(bufb), fb); if (na != nb || memcmp(bufa, bufb, na) != 0) { ac_log("[verify] MISMATCH at offset %ld (read %zu vs %zu)", verified, na, nb); flash_trace("verify: MISMATCH at offset=%ld src_read=%zu dst_read=%zu", verified, na, nb); fclose(fa); fclose(fb); return -1; } verified += (long)na; } // Make sure dst doesn't have extra bytes nb = fread(bufb, 1, 1, fb); if (nb != 0) { ac_log("[verify] dst has extra bytes after %ld", verified); flash_trace("verify: dst has extra bytes after %ld", verified); fclose(fa); fclose(fb); return -1; } fclose(fa); fclose(fb); ac_log("[verify] OK: %ld bytes match", verified); flash_trace("verify: OK %ld bytes match", verified); return verified;}
// Write a line to the flash telemetry ring buffer (thread-safe enough for single writer)static void flash_tlog(ACRuntime *rt, const char *fmt, ...) { va_list ap; va_start(ap, fmt); int idx = rt->flash_log_count % 16; vsnprintf(rt->flash_log[idx], 128, fmt, ap); va_end(ap); __sync_fetch_and_add(&rt->flash_log_count, 1);}
// Append a timestamped line to /tmp/flash-trace.log, which lives in tmpfs so// it can't be affected by the vfat partition we're flashing. On flash// completion (success OR failure) this file is copied to /mnt/flash-last.log// for post-mortem. This is INDEPENDENT of ac_log — the previous design// paused ac_log across the entire flash and lost all diagnostic output when// the flash thread hung or crashed. flash_trace NEVER pauses.static FILE *flash_trace_fp = NULL;static void flash_trace_open(void) { if (flash_trace_fp) { fclose(flash_trace_fp); flash_trace_fp = NULL; } // Truncate per-flash so we get a fresh log flash_trace_fp = fopen("/tmp/flash-trace.log", "w"); if (flash_trace_fp) { fprintf(flash_trace_fp, "=== flash-trace %ld ===\n", (long)time(NULL)); fflush(flash_trace_fp); }}static void flash_trace(const char *fmt, ...) { if (!flash_trace_fp) return; struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); fprintf(flash_trace_fp, "[%ld.%03ld] ", (long)ts.tv_sec, ts.tv_nsec / 1000000); va_list ap; va_start(ap, fmt); vfprintf(flash_trace_fp, fmt, ap); va_end(ap); if (fmt[0] && fmt[strlen(fmt) - 1] != '\n') fputc('\n', flash_trace_fp); fflush(flash_trace_fp); // No fsync — tmpfs doesn't need it and we want speed}static void flash_trace_close_and_archive(void) { if (flash_trace_fp) { fclose(flash_trace_fp); flash_trace_fp = NULL; } // Copy to /mnt/flash-last.log for post-mortem after flash completes. // If /mnt is mid-remount or busy, this might fail silently — that's OK, // the tmpfs copy is still available while the process lives. FILE *src = fopen("/tmp/flash-trace.log", "rb"); if (!src) return; FILE *dst = fopen("/mnt/flash-last.log", "wb"); if (dst) { char buf[4096]; size_t n; while ((n = fread(buf, 1, sizeof(buf), src)) > 0) fwrite(buf, 1, n, dst); fflush(dst); fsync(fileno(dst)); fclose(dst); } fclose(src);}
// Flash update background thread: mount EFI, copy vmlinuz, sync, umount// All operations use C syscalls — no shell commands (cp/mkdir/mount not in initramfs PATH)// NOTE: current_rt is __thread (thread-local); pass ACRuntime * via arg insteadstatic void *flash_thread_fn(void *arg) { ACRuntime *rt = (ACRuntime *)arg; if (!rt) return NULL; rt->flash_log_count = 0; rt->flash_dst[0] = '\0'; rt->flash_same_device = 0;
// Open a dedicated trace log in tmpfs. This survives even if the main // log gets suspended / the vfat fs we're flashing goes read-only / the // flash thread hangs. At the end of the thread (success or failure) we // copy it to /mnt/flash-last.log for post-mortem on the next boot. flash_trace_open(); flash_trace("flash_thread_fn start"); flash_trace("src=%s device=%s", rt->flash_src, rt->flash_device);
ac_log("[flash] starting: src=%s device=%s", rt->flash_src, rt->flash_device); flash_tlog(rt, "src=%s", rt->flash_src); flash_tlog(rt, "device=%s", rt->flash_device);
// Check if device node exists { struct stat dev_st; if (stat(rt->flash_device, &dev_st) != 0) { ac_log("[flash] device %s does not exist (errno=%d %s)", rt->flash_device, errno, strerror(errno)); flash_trace("device %s missing errno=%d", rt->flash_device, errno); flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; } ac_log("[flash] device %s exists (mode=0%o)", rt->flash_device, dev_st.st_mode); flash_trace("device exists mode=0%o", dev_st.st_mode); }
// Whole-disk target? If flash_device names a disk (no /sys/.../partition // file — that file only exists for partition nodes), partition + format // it first, then redirect flash_device to the new ESP partition. Used // for installing onto blank/unformatted SSDs the user just plugged in. { const char *base = strrchr(rt->flash_device, '/'); base = base ? base + 1 : rt->flash_device; char part_marker[128]; snprintf(part_marker, sizeof(part_marker), "/sys/class/block/%s/partition", base); int is_whole_disk = (access(part_marker, F_OK) != 0); if (is_whole_disk) { ac_log("[flash] %s is a whole disk — running format-and-install (sfdisk + mkfs.vfat)", rt->flash_device); flash_tlog(rt, "format-and-install: whole disk %s", rt->flash_device); // Wipe first 16 MiB so any old partition table / FS signatures // don't confuse sfdisk or the kernel after re-read. char cmd[512]; snprintf(cmd, sizeof(cmd), "dd if=/dev/zero of=%s bs=1M count=16 conv=fsync 2>&1", rt->flash_device); FILE *p = popen(cmd, "r"); if (p) { char buf[256]; while (fgets(buf, sizeof(buf), p)) flash_tlog(rt, "wipe: %s", buf); pclose(p); } // sfdisk: single GPT ESP spanning the whole disk. Type // C12A7328-F81F-11D2-BA4B-00A0C93EC93B = EFI System Partition. snprintf(cmd, sizeof(cmd), "echo 'label: gpt\\nname=ACBOOT,type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B\\n' | " "sfdisk --force --no-reread %s 2>&1", rt->flash_device); ac_log("[flash] sfdisk: %s", cmd); int sfdisk_rc = -1; p = popen(cmd, "r"); if (p) { char buf[256]; while (fgets(buf, sizeof(buf), p)) ac_log("[flash] sfdisk: %s", buf); sfdisk_rc = pclose(p); } ac_log("[flash] sfdisk exit=%d", sfdisk_rc); // Tell the kernel to re-read the partition table. char rereadcmd[256]; snprintf(rereadcmd, sizeof(rereadcmd), "blockdev --rereadpt %s 2>/dev/null; partx -u %s 2>/dev/null", rt->flash_device, rt->flash_device); system(rereadcmd); // Determine partition node name. NVMe + mmcblk use pN, sd* use N. char part_node[64]; if (strstr(rt->flash_device, "nvme") || strstr(rt->flash_device, "mmc")) snprintf(part_node, sizeof(part_node), "%sp1", rt->flash_device); else snprintf(part_node, sizeof(part_node), "%s1", rt->flash_device); // Wait up to 5s for the partition node to materialize. int part_ready = 0; for (int wait = 0; wait < 50; wait++) { if (access(part_node, F_OK) == 0) { part_ready = 1; break; } usleep(100000); } if (!part_ready) { ac_log("[flash] partition %s did not appear after sfdisk", part_node); flash_trace("partition %s missing after sfdisk", part_node); flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; } // Format the new ESP as FAT32. snprintf(cmd, sizeof(cmd), "mkfs.vfat -F 32 -n ACBOOT %s 2>&1", part_node); int mkfs_rc = -1; p = popen(cmd, "r"); if (p) { char buf[256]; while (fgets(buf, sizeof(buf), p)) ac_log("[flash] mkfs: %s", buf); mkfs_rc = pclose(p); } ac_log("[flash] mkfs.vfat %s exit=%d", part_node, mkfs_rc); // Redirect the rest of flash_thread_fn at the new partition. strncpy(rt->flash_device, part_node, sizeof(rt->flash_device) - 1); rt->flash_device[sizeof(rt->flash_device) - 1] = 0; ac_log("[flash] format-and-install: redirected to %s", rt->flash_device); } }
// Mount the EFI partition for flashing. // IMPORTANT: If the flash target is the same device already mounted at /mnt // (e.g. NVMe-to-NVMe OTA), we MUST use /mnt directly. Mounting the same // vfat block device twice causes FAT table corruption — each mount has its // own in-memory FAT, and umount of one will overwrite the other's changes. const char *efi_mount = NULL; int did_mount = 0;
// Check if flash target is already mounted at /mnt extern char log_dev[]; // set during setup_logging() int same_as_mnt = (log_dev[0] && strcmp(log_dev, rt->flash_device) == 0); rt->flash_same_device = same_as_mnt; flash_tlog(rt, "log_dev=%s same=%d", log_dev, same_as_mnt);
if (same_as_mnt) { // Same device as /mnt — use it directly, never double-mount if (access("/mnt/EFI/BOOT", F_OK) == 0 || access("/mnt/EFI", F_OK) == 0) { efi_mount = "/mnt"; ac_log("[flash] flash target = boot device (%s), using /mnt directly", rt->flash_device); } else { ac_log("[flash] flash target = boot device but /mnt has no EFI dir"); // Create EFI structure on /mnt mkdir("/mnt/EFI", 0755); mkdir("/mnt/EFI/BOOT", 0755); if (access("/mnt/EFI/BOOT", F_OK) == 0) { efi_mount = "/mnt"; ac_log("[flash] created EFI/BOOT on /mnt"); } } }
if (!efi_mount) { // Different device — mount fresh at /tmp/efi mkdir("/tmp/efi", 0755); umount("/tmp/efi"); // clean up any stale mount
const char *fstypes[] = {"vfat", "ext4", "ext2", NULL}; for (int fi = 0; fstypes[fi] && !did_mount; fi++) { int mr = mount(rt->flash_device, "/tmp/efi", fstypes[fi], 0, NULL); if (mr == 0) { efi_mount = "/tmp/efi"; did_mount = 1; ac_log("[flash] mounted %s at /tmp/efi (type=%s)", rt->flash_device, fstypes[fi]); } else { ac_log("[flash] mount %s as %s failed: errno=%d (%s)", rt->flash_device, fstypes[fi], errno, strerror(errno)); } }
// If all mounts failed and this is NVMe, try formatting as vfat (fresh drive) if (!did_mount && strstr(rt->flash_device, "nvme")) { ac_log("[flash] NVMe mount failed — attempting mkfs.vfat on %s", rt->flash_device); char cmd[256]; snprintf(cmd, sizeof(cmd), "mkfs.vfat -F 32 -n ACBOOT %s 2>&1", rt->flash_device); FILE *p = popen(cmd, "r"); if (p) { char buf[256]; while (fgets(buf, sizeof(buf), p)) ac_log("[flash] mkfs: %s", buf); int rc = pclose(p); ac_log("[flash] mkfs exit=%d", rc); if (rc == 0) { if (mount(rt->flash_device, "/tmp/efi", "vfat", 0, NULL) == 0) { efi_mount = "/tmp/efi"; did_mount = 1; ac_log("[flash] mounted fresh vfat on %s", rt->flash_device); } } } }
// Last resort: check /mnt if (!did_mount) { if (access("/mnt/EFI/BOOT", F_OK) == 0) { efi_mount = "/mnt"; ac_log("[flash] using existing /mnt mount (fresh mount failed)"); flash_trace("falling back to existing /mnt mount"); } else { ac_log("[flash] ABORT: mount %s failed and /mnt has no EFI/BOOT", rt->flash_device); flash_trace("ABORT mount %s failed and no /mnt/EFI/BOOT", rt->flash_device); flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; } } } flash_trace("efi_mount=%s did_mount=%d", efi_mount, did_mount); // Create EFI directory structure if it doesn't exist (fresh NVMe install) char efi_dir[512]; snprintf(efi_dir, sizeof(efi_dir), "%s/EFI", efi_mount); mkdir(efi_dir, 0755); snprintf(efi_dir, sizeof(efi_dir), "%s/EFI/BOOT", efi_mount); mkdir(efi_dir, 0755); if (access(efi_dir, F_OK) != 0) { ac_log("[flash] ABORT: %s could not be created", efi_dir); flash_trace("ABORT could not create %s", efi_dir); flash_trace_close_and_archive(); if (did_mount) umount("/tmp/efi"); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; }
// If splash chainloader is present, kernel lives at KERNEL.EFI; else BOOTX64.EFI char dst[512]; char kernel_path[512]; snprintf(kernel_path, sizeof(kernel_path), "%s/EFI/BOOT/KERNEL.EFI", efi_mount); if (access(kernel_path, F_OK) == 0) { snprintf(dst, sizeof(dst), "%s", kernel_path); ac_log("[flash] chainloader detected, updating KERNEL.EFI"); } else { snprintf(dst, sizeof(dst), "%s/EFI/BOOT/BOOTX64.EFI", efi_mount); ac_log("[flash] no chainloader, updating BOOTX64.EFI"); } ac_log("[flash] destination: %s (efi_mount=%s, same_device=%d, did_mount=%d)", dst, efi_mount, same_as_mnt, did_mount); strncpy(rt->flash_dst, dst, sizeof(rt->flash_dst) - 1); rt->flash_dst[sizeof(rt->flash_dst) - 1] = '\0'; flash_tlog(rt, "dst=%s mount=%s", dst, efi_mount);
// NOTE: we intentionally no longer ac_log_pause() the main log during // the flash. The old design closed /mnt/ac-native.log while writing to // the same vfat partition, which caused every flash failure to leave // zero diagnostic trail — if the thread hung or the mount went // read-only mid-write, ac_log_resume() never fired and the entire // session's logs after the pause were silently dropped. Instead we // write flash progress to flash_trace (tmpfs, can't be affected by the // partition we're flashing) AND best-effort to the main log. At the // end we archive the trace to /mnt/flash-last.log. int same_mount = same_as_mnt || !did_mount; flash_trace("same_mount=%d same_as_mnt=%d did_mount=%d", same_mount, same_as_mnt, did_mount);
// Pre-flight: validate source file exists and has reasonable size long kernel_src_size = -1; long initramfs_src_size = -1; { struct stat src_st; if (stat(rt->flash_src, &src_st) != 0 || src_st.st_size < 1048576) { ac_log("[flash] ABORT: source %s invalid (size=%ld, errno=%d)", rt->flash_src, (long)(src_st.st_size), errno); flash_trace("ABORT source invalid size=%ld errno=%d", (long)(src_st.st_size), errno); flash_tlog(rt, "ABORT kernel src invalid size=%ld", (long)(src_st.st_size)); flash_trace_close_and_archive(); if (did_mount) umount("/tmp/efi"); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; } kernel_src_size = (long)src_st.st_size; ac_log("[flash] source validated: %ld bytes", kernel_src_size); flash_trace("source validated size=%ld", kernel_src_size); } if (rt->flash_initramfs_src[0]) { struct stat ir_st; if (stat(rt->flash_initramfs_src, &ir_st) != 0 || ir_st.st_size < 1048576) { ac_log("[flash] ABORT: initramfs source %s invalid (size=%ld errno=%d)", rt->flash_initramfs_src, (long)(ir_st.st_size), errno); flash_tlog(rt, "ABORT initramfs src invalid size=%ld", (long)(ir_st.st_size)); flash_trace_close_and_archive(); if (did_mount) umount("/tmp/efi"); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; } initramfs_src_size = (long)ir_st.st_size; ac_log("[flash] initramfs source validated: %ld bytes", initramfs_src_size); flash_trace("initramfs source validated size=%ld", initramfs_src_size); }
// Pre-flight: total-space check. Compute the worst-case footprint of the // operation (kernel + initramfs + 4 MB margin) and abort BEFORE we start // touching the partition. Without this, the kernel write (which uses // rename-old→.prev double-occupancy) can succeed but leave too little // room for the initramfs write, leading to a kernel/initramfs mismatch // that boots black-screen or panics. Existing initramfs counts as // recoverable space because its write path unlinks before writing. { struct statvfs vfs; if (statvfs(efi_mount, &vfs) == 0) { long free_now = (long)vfs.f_bavail * (long)vfs.f_bsize; char ir_dst[512]; ir_dst[0] = '\0'; long ir_existing = 0; if (rt->flash_initramfs_src[0]) { snprintf(ir_dst, sizeof(ir_dst), "%s/initramfs.cpio.gz", efi_mount); struct stat ir_dst_st; if (stat(ir_dst, &ir_dst_st) == 0) ir_existing = (long)ir_dst_st.st_size; } // Effective free = free + (initramfs unlink before write) long effective_free = free_now + ir_existing; // Need = kernel-with-backup + initramfs (no backup) + 4 MB margin long need = kernel_src_size + 4 * 1048576; if (initramfs_src_size > 0) need += initramfs_src_size; ac_log("[flash] preflight: free=%ldMB existing_initramfs=%ldMB effective=%ldMB need=%ldMB", free_now / 1048576, ir_existing / 1048576, effective_free / 1048576, need / 1048576); flash_tlog(rt, "preflight free=%ldMB need=%ldMB", effective_free / 1048576, need / 1048576); if (effective_free < need) { ac_log("[flash] ABORT preflight: insufficient ESP space (free %ldMB < need %ldMB)", effective_free / 1048576, need / 1048576); flash_tlog(rt, "ABORT preflight insufficient space"); flash_trace_close_and_archive(); if (did_mount) umount("/tmp/efi"); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; } } else { ac_log("[flash] preflight: statvfs failed errno=%d — proceeding anyway", errno); flash_tlog(rt, "preflight statvfs FAILED errno=%d", errno); } }
// Phase 1: Backup previous kernel, then write new one rt->flash_phase = 1;
// Keep previous version as .prev for rollback { char prev[512]; snprintf(prev, sizeof(prev), "%s.prev", dst); if (access(dst, F_OK) == 0) { // Remove old .prev, rename current to .prev unlink(prev); if (rename(dst, prev) == 0) { ac_log("[flash] backed up previous kernel to %s", prev); flash_tlog(rt, "backup=%s", prev); } else { ac_log("[flash] backup rename failed (errno=%d), continuing anyway", errno); } } }
flash_tlog(rt, "writing %s -> %s", rt->flash_src, dst); long copied = flash_copy_file(rt->flash_src, dst); flash_tlog(rt, "wrote %ld bytes", copied);
// Kernel size-match verification — mirror the initramfs check so a short // write can never be silently treated as success. If the write was short, // restore the .prev backup so the device can still boot the previous // kernel + (still-intact) initramfs combination. if (copied <= 0 || (kernel_src_size > 0 && copied != kernel_src_size)) { ac_log("[flash] kernel copy SHORT/FAILED (wrote=%ld src=%ld) — restoring .prev", copied, kernel_src_size); flash_tlog(rt, "kernel write SHORT wrote=%ld src=%ld", copied, kernel_src_size); unlink(dst); char prev_restore[512]; snprintf(prev_restore, sizeof(prev_restore), "%s.prev", dst); if (access(prev_restore, F_OK) == 0) { if (rename(prev_restore, dst) == 0) { ac_log("[flash] restored previous kernel from %s", prev_restore); flash_tlog(rt, "restored kernel from .prev"); } } flash_trace_close_and_archive(); if (did_mount) umount("/tmp/efi"); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; }
// Also update Microsoft Boot Manager path (ThinkPad BIOS often boots this first) // Check available space first — don't write second copy if it won't fit. // FAT32 can't hardlink, so we need actual space for both copies. { char ms_dir[512], ms_dst[512]; snprintf(ms_dir, sizeof(ms_dir), "%s/EFI/Microsoft", efi_mount); mkdir(ms_dir, 0755); snprintf(ms_dir, sizeof(ms_dir), "%s/EFI/Microsoft/Boot", efi_mount); mkdir(ms_dir, 0755); snprintf(ms_dst, sizeof(ms_dst), "%s/EFI/Microsoft/Boot/bootmgfw.efi", efi_mount);
// Check free space on partition before writing second copy struct statvfs vfs; long free_bytes = 0; if (statvfs(efi_mount, &vfs) == 0) { free_bytes = (long)vfs.f_bavail * (long)vfs.f_bsize; } long need_bytes = copied + (1024 * 1024); // need kernel size + 1MB margin
if (free_bytes >= need_bytes) { // Backup old MS boot path too char ms_prev[512]; snprintf(ms_prev, sizeof(ms_prev), "%s.prev", ms_dst); unlink(ms_prev); if (access(ms_dst, F_OK) == 0) { rename(ms_dst, ms_prev); // safe: old version preserved } long ms_copied = flash_copy_file(rt->flash_src, ms_dst); if (ms_copied > 0) { unlink(ms_prev); // success: remove backup ac_log("[flash] also wrote Microsoft boot path: %ld bytes -> %s", ms_copied, ms_dst); flash_tlog(rt, "ms_boot=%ld", ms_copied); } else { // Write failed — restore backup if (access(ms_prev, F_OK) == 0) { rename(ms_prev, ms_dst); ac_log("[flash] MS boot write failed, restored previous"); } } } else { ac_log("[flash] skipping MS boot path: %ldMB free < %ldMB needed", free_bytes / 1048576, need_bytes / 1048576); flash_tlog(rt, "ms_boot=skipped (space)"); } }
// Optional initramfs copy — for OTA updates where the kernel is now // slim (Phase 2 de-embed) and must load `\initramfs.cpio.gz` from the // ESP root via its baked-in `initrd=` cmdline. Without this step, an // os.mjs OTA would leave the new kernel paired with the PREVIOUS // initramfs, which is a latent source of boot breakage whenever any // file inside initramfs changes. if (rt->flash_initramfs_src[0]) { char initramfs_dst[512]; snprintf(initramfs_dst, sizeof(initramfs_dst), "%s/initramfs.cpio.gz", efi_mount); ac_log("[flash] writing initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); flash_tlog(rt, "initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst);
// CRITICAL: initramfs is 326 MB on a typical 600 MB ESP. The previous // approach (rename old → .prev, then write new) double-occupied ~650 // MB and the new write hit ENOSPC mid-stream. flash_copy_file // returned the partial byte count (positive), the `<= 0` failure // check passed, OS reported "installed", but the corrupt initramfs // panicked on next boot with "initramfs unpacking failed: read error". // // Fix: skip the .prev backup for initramfs (it's too big to keep // alongside the new copy on tight ESPs). Unlink old, write new, // verify byte count matches source. If write was short, abort // hard so the OS reports failure instead of silently shipping // a corrupt boot. long src_size = initramfs_src_size; // already stat'd in preflight // Free space + size of file we're about to delete = effective free. struct stat dst_st; long dst_existing = (stat(initramfs_dst, &dst_st) == 0) ? (long)dst_st.st_size : 0; struct statvfs vfs; long free_after_unlink = 0; if (statvfs(efi_mount, &vfs) == 0) { free_after_unlink = (long)vfs.f_bavail * (long)vfs.f_bsize + dst_existing; } ac_log("[flash] initramfs space: src=%ldMB free_after_unlink=%ldMB existing=%ldMB", src_size / 1048576, free_after_unlink / 1048576, dst_existing / 1048576); flash_tlog(rt, "initramfs space src=%ldMB free=%ldMB", src_size / 1048576, free_after_unlink / 1048576); if (src_size > 0 && free_after_unlink < src_size + (4 * 1048576)) { ac_log("[flash] initramfs would NOT fit — need %ldMB, free_after_unlink %ldMB", src_size / 1048576, free_after_unlink / 1048576); flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; } // Also remove old kernel .prev — gives a few more MB of headroom // and we don't expose rollback to JS anyway. char kernel_prev[512]; snprintf(kernel_prev, sizeof(kernel_prev), "%s.prev", dst); unlink(kernel_prev); // Remove the old initramfs entirely BEFORE writing new (no .prev // double-occupation). unlink(initramfs_dst); sync(); long initramfs_copied = flash_copy_file(rt->flash_initramfs_src, initramfs_dst); flash_tlog(rt, "initramfs wrote %ld bytes (src=%ld)", initramfs_copied, src_size); // Verify byte count matches. flash_copy_file returns SHORT count on // ENOSPC mid-stream — without this check we'd ship a broken initramfs. if (initramfs_copied <= 0 || (src_size > 0 && initramfs_copied != src_size)) { ac_log("[flash] initramfs copy SHORT/FAILED (wrote=%ld src=%ld)", initramfs_copied, src_size); // Don't try to restore — old initramfs already deleted. Mark // failure so OS reports it instead of pretending success. unlink(initramfs_dst); flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; } }
// Phase 2: Syncing to disk — belt-and-suspenders for vfat rt->flash_phase = 2;
// Force page cache eviction of the written file so subsequent reads hit disk { int dst_fd = open(dst, O_RDONLY); if (dst_fd >= 0) { struct stat st; if (fstat(dst_fd, &st) == 0) { posix_fadvise(dst_fd, 0, st.st_size, POSIX_FADV_DONTNEED); ac_log("[flash] evicted written file from page cache (%ld bytes)", (long)st.st_size); } close(dst_fd); } }
// syncfs + multi-round sync with generous waits. USB flash controllers // can take SECONDS to flush 272 MB through vfat metadata, and the old // 500 ms window was frequently insufficient — we'd then try to verify // against data that still existed only in the kernel's dirty page // cache, so the eviction step exposed a mismatch with uncommitted // backing store bytes. int mnt_fd = open(efi_mount, O_RDONLY); if (mnt_fd >= 0) { int sr = syncfs(mnt_fd); if (sr != 0) { ac_log("[flash] syncfs failed: errno=%d (%s)", errno, strerror(errno)); flash_trace("syncfs FAILED errno=%d", errno); } else { flash_trace("syncfs ok"); } close(mnt_fd); } else { ac_log("[flash] WARNING: open(%s) for syncfs failed errno=%d", efi_mount, errno); flash_trace("open(efi_mount) for syncfs FAILED errno=%d", errno); } sync(); flash_trace("sync #1 returned"); // First wait — let the block layer start flushing usleep(1500000); // 1.5 s sync(); flash_trace("sync #2 after 1.5s wait"); // Second wait — USB flash write can be very slow for 272 MB usleep(1500000); // another 1.5 s = 3 s total sync(); flash_trace("sync #3 after 3s total wait");
// Previously we remounted /mnt ro+rw to force vfat metadata flush. That // was fragile: it bypassed the VFS safely-unmount path, briefly made // /mnt read-only so any concurrent log write would fail, and on systems // with mount stacking (init.sh leaves nvme0n1p1 stacked under sda1) // the remount hit the wrong layer. syncfs + sync + 3 s of settle time // is more reliable. ac_log("[flash] sync complete"); flash_trace("sync complete (3 rounds, 3s total wait)");
if (copied <= 0) { ac_log("[flash] copy failed (copied=%ld)", copied); flash_trace("copy FAILED copied=%ld", copied); flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; }
// Phase 3: Verify — read back from physical disk and compare byte-for-byte. // flash_verify now does a size-check first (fast, diagnostic) before // paying the cost of a 272 MB read+compare. rt->flash_phase = 3; flash_trace("verify starting (expecting %ld bytes)", copied); long verified = flash_verify(rt->flash_src, dst); flash_tlog(rt, "verify=%ld (expected=%ld)", verified, copied); flash_trace("verify returned: %ld", verified);
if (did_mount) { umount("/tmp/efi"); ac_log("[flash] unmounted /tmp/efi"); flash_trace("unmounted /tmp/efi"); }
rt->flash_verified_bytes = verified; if (verified != copied) { ac_log("[flash] VERIFY FAILED: wrote %ld, verified %ld", copied, verified); flash_trace("VERIFY FAILED wrote=%ld verified=%ld", copied, verified); // CRITICAL: restore previous kernel so device remains bootable { char prev[512]; snprintf(prev, sizeof(prev), "%s.prev", dst); if (access(prev, F_OK) == 0) { unlink(dst); if (rename(prev, dst) == 0) { ac_log("[flash] RESTORED previous kernel from %s — device still bootable", prev); flash_trace("RESTORED previous kernel from %s", prev); sync(); } else { ac_log("[flash] CRITICAL: restore failed errno=%d", errno); flash_trace("CRITICAL restore FAILED errno=%d", errno); } } else { flash_trace("no .prev backup to restore (device may be unbootable)"); } } flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 0; rt->flash_pending = 0; rt->flash_done = 1; return NULL; }
// Set UEFI boot order so firmware boots our kernel (not a stale vendor entry). // Many OEM firmwares (HP, Dell, Lenovo) have hardcoded boot entries pointing at // vendor-specific EFI paths. Without updating the boot order, the firmware may // boot an old kernel (e.g. from a previous OS install) instead of ours. { // Mount efivarfs if not already mounted (needed to write UEFI variables) struct stat evs; if (stat("/sys/firmware/efi/efivars", &evs) == 0) { // Try mounting (harmless if already mounted) mount("efivarfs", "/sys/firmware/efi/efivars", "efivarfs", 0, NULL);
// Extract disk device from partition path for efibootmgr --disk // e.g. /dev/sda1 -> /dev/sda, /dev/nvme0n1p1 -> /dev/nvme0n1 char disk[64] = "", partnum[8] = ""; strncpy(disk, rt->flash_device, sizeof(disk) - 1); char *p = disk + strlen(disk) - 1; // Walk back past the partition number while (p > disk && *p >= '0' && *p <= '9') p--; if (p > disk) { strncpy(partnum, p + 1, sizeof(partnum) - 1); // For NVMe: /dev/nvme0n1p1 -> strip 'p' before part number if (*p == 'p' && p > disk && *(p-1) >= '0' && *(p-1) <= '9') *p = '\0'; else *(p + 1) = '\0'; }
// Use efibootmgr if available (cleanest approach) char cmd[512]; snprintf(cmd, sizeof(cmd), "efibootmgr -c -d %s -p %s -L 'AC Native OS' " "-l '\\EFI\\BOOT\\BOOTX64.EFI' 2>&1", disk, partnum[0] ? partnum : "1"); ac_log("[flash] setting UEFI boot entry: %s", cmd); flash_tlog(rt, "efibootmgr: %s %s part=%s", disk, partnum, partnum[0] ? partnum : "1");
FILE *efip = popen(cmd, "r"); if (efip) { char buf[256]; while (fgets(buf, sizeof(buf), efip)) { // Trim newline char *nl = strchr(buf, '\n'); if (nl) *nl = '\0'; ac_log("[flash] efibootmgr: %s", buf); flash_tlog(rt, "efi: %s", buf); } int rc = pclose(efip); if (rc == 0) { ac_log("[flash] UEFI boot entry created successfully"); flash_tlog(rt, "efi_boot=ok"); } else { ac_log("[flash] efibootmgr exit=%d (boot entry may not be set)", rc); flash_tlog(rt, "efi_boot=fail(%d)", rc); } } else { ac_log("[flash] efibootmgr not available — UEFI boot order unchanged"); flash_tlog(rt, "efi_boot=no_efibootmgr"); } } else { ac_log("[flash] no EFI variables support — skipping boot order update"); } }
// Remove downloaded file to free /tmp space unlink(rt->flash_src); ac_log("[flash] done: %ld bytes written, verified OK", copied); flash_trace("done: %ld bytes verified OK", copied); flash_trace_close_and_archive(); rt->flash_phase = 4; rt->flash_ok = 1; rt->flash_pending = 0; rt->flash_done = 1; return NULL;}
// ─────────────────────────────────────────────────────────────────// Audio diagnostic — list PCMs + play a short test tone on an arbitrary// device. Used by the speaker.mjs piece to figure out which ALSA PCM// actually produces sound on a given machine (vs wiring-dependent guesses// like hw:0,0). The tone plays in a detached thread so the JS caller// returns immediately; the piece UI stays responsive.// ─────────────────────────────────────────────────────────────────
static JSValue js_audio_list_pcms(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; JSValue arr = JS_NewArray(ctx); int idx = 0; for (int c = 0; c < 4; c++) { for (int d = 0; d < 8; d++) { char path[80]; snprintf(path, sizeof(path), "/proc/asound/card%d/pcm%dp/info", c, d); FILE *fp = fopen(path, "r"); if (!fp) continue; char line[256], id_str[96] = "", name_str[96] = ""; while (fgets(line, sizeof(line), fp)) { char *nl = strchr(line, '\n'); if (nl) *nl = 0; if (!strncmp(line, "id: ", 4)) snprintf(id_str, sizeof(id_str), "%s", line + 4); else if (!strncmp(line, "name: ", 6)) snprintf(name_str, sizeof(name_str), "%s", line + 6); } fclose(fp); char dev[16]; snprintf(dev, sizeof(dev), "hw:%d,%d", c, d); JSValue obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, obj, "device", JS_NewString(ctx, dev)); JS_SetPropertyStr(ctx, obj, "card", JS_NewInt32(ctx, c)); JS_SetPropertyStr(ctx, obj, "num", JS_NewInt32(ctx, d)); JS_SetPropertyStr(ctx, obj, "id", JS_NewString(ctx, id_str)); JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, name_str)); /* Tag the PCM that ac-native's main audio thread picked so the * UI can highlight it. */ if (current_rt && current_rt->audio && strcmp(current_rt->audio->audio_device, dev) == 0) JS_SetPropertyStr(ctx, obj, "active", JS_NewBool(ctx, 1)); JS_SetPropertyUint32(ctx, arr, idx++, obj); } } return arr;}
struct audio_probe_args { char device[32]; int freq_hz; int duration_ms; float volume;};
static void *audio_probe_thread(void *arg) { struct audio_probe_args *a = (struct audio_probe_args *)arg; snd_pcm_t *pcm = NULL; int err = snd_pcm_open(&pcm, a->device, SND_PCM_STREAM_PLAYBACK, 0); if (err < 0) { ac_log("[audio-probe] open %s failed: %s", a->device, snd_strerror(err)); free(a); return NULL; } unsigned int rate = 48000; snd_pcm_uframes_t period = 480, buffer = 1920; err = snd_pcm_set_params(pcm, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, 2, /* stereo */ rate, 1, /* soft_resample */ 100 * 1000); /* 100ms latency */ if (err < 0) { ac_log("[audio-probe] set_params %s failed: %s", a->device, snd_strerror(err)); snd_pcm_close(pcm); free(a); return NULL; } /* Synthesize + play a sine wave of a->freq_hz at a->volume for * a->duration_ms milliseconds. Stereo S16 LE. */ int total_frames = (long long)rate * a->duration_ms / 1000; int16_t *buf = (int16_t *)malloc(period * 2 * sizeof(int16_t)); if (!buf) { snd_pcm_close(pcm); free(a); return NULL; } double phase = 0.0; double step = 2.0 * 3.14159265358979 * (double)a->freq_hz / (double)rate; int32_t amp = (int32_t)(a->volume * 32760.0); if (amp < 0) amp = 0; if (amp > 32760) amp = 32760; int written = 0; ac_log("[audio-probe] %s %dHz %dms vol=%.2f", a->device, a->freq_hz, a->duration_ms, a->volume); while (written < total_frames) { int chunk = total_frames - written; if (chunk > (int)period) chunk = (int)period; for (int i = 0; i < chunk; i++) { int16_t s = (int16_t)(sin(phase) * amp); buf[i * 2] = s; buf[i * 2 + 1] = s; phase += step; if (phase > 6.283185307179586) phase -= 6.283185307179586; } snd_pcm_sframes_t w = snd_pcm_writei(pcm, buf, chunk); if (w < 0) w = snd_pcm_recover(pcm, w, 1); if (w < 0) { ac_log("[audio-probe] writei failed: %s", snd_strerror((int)w)); break; } written += (int)w; } snd_pcm_drain(pcm); snd_pcm_close(pcm); free(buf); ac_log("[audio-probe] %s done (%d frames)", a->device, written); free(a); return NULL;}
static JSValue js_audio_test_pcm(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_FALSE; struct audio_probe_args *a = (struct audio_probe_args *)calloc(1, sizeof(*a)); if (!a) return JS_FALSE; const char *dev = JS_ToCString(ctx, argv[0]); if (!dev) { free(a); return JS_FALSE; } strncpy(a->device, dev, sizeof(a->device) - 1); JS_FreeCString(ctx, dev); a->freq_hz = 440; a->duration_ms = 500; a->volume = 0.3f; if (argc >= 2) JS_ToInt32(ctx, &a->freq_hz, argv[1]); if (argc >= 3) JS_ToInt32(ctx, &a->duration_ms, argv[2]); if (argc >= 4) { double vol; JS_ToFloat64(ctx, &vol, argv[3]); a->volume = (float)vol; } pthread_t th; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); int pr = pthread_create(&th, &attr, audio_probe_thread, a); pthread_attr_destroy(&attr); if (pr != 0) { free(a); return JS_FALSE; } return JS_TRUE;}
// ─────────────────────────────────────────────────────────────────// Firmware update thread — runs /bin/ac-firmware-install (bundled copy of// system/public/install-firmware.sh) and streams its stdout into// rt->fw_log. The script handles all the dangerous bits (backup via// flashrom -r, CBFS swap via cbfstool, flashrom -w) — we just orchestrate// + surface progress. Availability is gated by os.mjs before this runs.// ─────────────────────────────────────────────────────────────────static void fw_log_line(ACRuntime *rt, const char *line) { int idx = rt->fw_log_count % 32; strncpy(rt->fw_log[idx], line, sizeof(rt->fw_log[idx]) - 1); rt->fw_log[idx][sizeof(rt->fw_log[idx]) - 1] = 0; __sync_fetch_and_add(&rt->fw_log_count, 1); ac_log("[fw] %s", line);}
static void *fw_thread_fn(void *arg) { ACRuntime *rt = (ACRuntime *)arg; if (!rt) return NULL; rt->fw_log_count = 0; rt->fw_backup_path[0] = 0; if (access("/bin/ac-firmware-install", X_OK) != 0) { fw_log_line(rt, "ac-firmware-install not bundled in initramfs"); rt->fw_ok = 0; rt->fw_pending = 0; rt->fw_done = 1; return NULL; } char cmd[512]; snprintf(cmd, sizeof(cmd), "AC_CDN=https://aesthetic.computer " "SPLASH_URL=https://oven.aesthetic.computer/firmware/splash.bmp " "/bin/ac-firmware-install %s 2>&1", rt->fw_args[0] ? rt->fw_args : ""); fw_log_line(rt, "starting ac-firmware-install"); FILE *p = popen(cmd, "r"); if (!p) { fw_log_line(rt, "popen failed"); rt->fw_ok = 0; rt->fw_pending = 0; rt->fw_done = 1; return NULL; } char line[256]; while (fgets(line, sizeof(line), p)) { char *nl = strchr(line, '\n'); if (nl) *nl = 0; // Capture the backup path line so JS can show it for "copy to USB" const char *bk = strstr(line, "/tmp/firmware-backup-"); if (bk) { int i = 0; while (bk[i] && bk[i] != ' ' && i < (int)sizeof(rt->fw_backup_path) - 1) { rt->fw_backup_path[i] = bk[i]; i++; } rt->fw_backup_path[i] = 0; } fw_log_line(rt, line); } int rc = pclose(p); rt->fw_ok = (WIFEXITED(rc) && WEXITSTATUS(rc) == 0) ? 1 : 0; char finish[64]; snprintf(finish, sizeof(finish), "ac-firmware-install exit=%d", WIFEXITED(rc) ? WEXITSTATUS(rc) : -1); fw_log_line(rt, finish); rt->fw_pending = 0; rt->fw_done = 1; return NULL;}
// system.firmwareInstall([mode]) — mode: "install" (default), "dry-run",// "restore". Returns immediately; poll system.firmware.{pending,done,ok,log}.static JSValue js_firmware_install(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt) return JS_UNDEFINED; if (current_rt->fw_pending) { ac_log("[fw] refused: already running"); return JS_NewBool(ctx, 0); } current_rt->fw_args[0] = 0; if (argc >= 1 && JS_IsString(argv[0])) { const char *mode = JS_ToCString(ctx, argv[0]); if (mode) { if (strcmp(mode, "dry-run") == 0) { strncpy(current_rt->fw_args, "--dry-run", sizeof(current_rt->fw_args) - 1); } else if (strcmp(mode, "restore") == 0) { strncpy(current_rt->fw_args, "--restore", sizeof(current_rt->fw_args) - 1); } JS_FreeCString(ctx, mode); } } current_rt->fw_pending = 1; current_rt->fw_done = 0; current_rt->fw_ok = 0; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); int pr = pthread_create(¤t_rt->fw_thread, &attr, fw_thread_fn, current_rt); pthread_attr_destroy(&attr); if (pr != 0) { current_rt->fw_pending = 0; current_rt->fw_done = 1; return JS_NewBool(ctx, 0); } return JS_NewBool(ctx, 1);}
// system.flashUpdate(srcPath[, devicePath[, initramfsSrcPath]])// devicePath defaults to auto-detected boot device if omitted.// initramfsSrcPath is optional — when provided, the flash thread also copies// it to `<ESP>/initramfs.cpio.gz` (matching the kernel's baked-in// `initrd=\initramfs.cpio.gz` cmdline). Required for OTA updates since the// kernel no longer embeds initramfs (Phase 2 de-embed).static JSValue js_flash_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 1) { ac_log("[flash] flashUpdate called with no runtime/args\n"); return JS_UNDEFINED; } const char *src = JS_ToCString(ctx, argv[0]); if (!src) { ac_log("[flash] flashUpdate: null src arg\n"); return JS_UNDEFINED; } ac_log("[flash] flashUpdate called: src=%s\n", src); current_rt->flash_pending = 1; current_rt->flash_done = 0; current_rt->flash_ok = 0; strncpy(current_rt->flash_src, src, sizeof(current_rt->flash_src) - 1); current_rt->flash_src[sizeof(current_rt->flash_src) - 1] = 0; JS_FreeCString(ctx, src); current_rt->flash_initramfs_src[0] = '\0'; // Device: use arg[1] if provided, else auto-detect if (argc >= 2 && !JS_IsUndefined(argv[1]) && !JS_IsNull(argv[1])) { const char *dev = JS_ToCString(ctx, argv[1]); if (dev) { strncpy(current_rt->flash_device, dev, sizeof(current_rt->flash_device) - 1); current_rt->flash_device[sizeof(current_rt->flash_device) - 1] = 0; JS_FreeCString(ctx, dev); } } else { detect_boot_device(current_rt->flash_device, sizeof(current_rt->flash_device)); } // Optional initramfs source — arg[2] if (argc >= 3 && !JS_IsUndefined(argv[2]) && !JS_IsNull(argv[2])) { const char *init_src = JS_ToCString(ctx, argv[2]); if (init_src) { strncpy(current_rt->flash_initramfs_src, init_src, sizeof(current_rt->flash_initramfs_src) - 1); current_rt->flash_initramfs_src[sizeof(current_rt->flash_initramfs_src) - 1] = 0; ac_log("[flash] initramfs src=%s", init_src); JS_FreeCString(ctx, init_src); } } // Pass runtime as arg (current_rt is thread-local, unavailable in new thread) pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); int pr = pthread_create(¤t_rt->flash_thread, &attr, flash_thread_fn, current_rt); pthread_attr_destroy(&attr); if (pr != 0) { ac_log("[flash] pthread_create failed (%d)", pr); current_rt->flash_ok = 0; current_rt->flash_pending = 0; current_rt->flash_done = 1; } return JS_UNDEFINED;}
// system.diskFreeBytes(path) — returns available bytes on the filesystem// holding `path`, or -1 on stat failure. Used by os.mjs preflight to refuse// an OTA install when the ESP would not fit kernel + initramfs without// hitting ENOSPC mid-write (the failure mode that historically silently// truncated the on-disk initramfs and panicked on next boot).static JSValue js_disk_free_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_NewInt64(ctx, -1); const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_NewInt64(ctx, -1); struct statvfs vfs; int rc = statvfs(path, &vfs); JS_FreeCString(ctx, path); if (rc != 0) return JS_NewInt64(ctx, -1); return JS_NewInt64(ctx, (int64_t)vfs.f_bavail * (int64_t)vfs.f_bsize);}
// system.fileSizeBytes(path) — returns size of file at `path`, or -1 if it// doesn't exist / cannot be stat'd. Used by os.mjs to verify a downloaded// kernel/initramfs matches the size declared in the .version file before// handing it to flashUpdate (catches truncated downloads).static JSValue js_file_size_bytes(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_NewInt64(ctx, -1); const char *path = JS_ToCString(ctx, argv[0]); if (!path) return JS_NewInt64(ctx, -1); struct stat st; int rc = stat(path, &st); JS_FreeCString(ctx, path); if (rc != 0) return JS_NewInt64(ctx, -1); return JS_NewInt64(ctx, (int64_t)st.st_size);}
// system.reboot() — triggers system reboot (direct syscall, no external binary needed)// system.setPowerRole("port0", "source"|"sink") — swap USB-C power rolestatic JSValue js_set_power_role(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2) return JS_NewBool(ctx, 0); const char *port = JS_ToCString(ctx, argv[0]); const char *role = JS_ToCString(ctx, argv[1]); if (!port || !role) { if (port) JS_FreeCString(ctx, port); if (role) JS_FreeCString(ctx, role); return JS_NewBool(ctx, 0); } // Validate role if (strcmp(role, "source") != 0 && strcmp(role, "sink") != 0) { JS_FreeCString(ctx, port); JS_FreeCString(ctx, role); return JS_NewBool(ctx, 0); } // Validate port name (must be "portN") if (strncmp(port, "port", 4) != 0 || !port[4]) { JS_FreeCString(ctx, port); JS_FreeCString(ctx, role); return JS_NewBool(ctx, 0); } char path[256]; snprintf(path, sizeof(path), "/sys/class/typec/%s/power_role", port); FILE *f = fopen(path, "w"); int ok = 0; if (f) { ok = fprintf(f, "%s\n", role) > 0; fclose(f); ac_log("[typec] setPowerRole %s -> %s: %s", port, role, ok ? "ok" : "write failed"); } else { ac_log("[typec] setPowerRole: cannot open %s: %s", path, strerror(errno)); } JS_FreeCString(ctx, port); JS_FreeCString(ctx, role); return JS_NewBool(ctx, ok);}
static JSValue build_usb_midi_state_obj(JSContext *ctx) { ACUsbMidiState state; int has_state = usb_midi_read_state(&state); if (!has_state) { memset(&state, 0, sizeof(state)); snprintf(state.reason, sizeof(state.reason), "uninitialized"); }
JSValue obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, obj, "enabled", JS_NewBool(ctx, state.enabled)); JS_SetPropertyStr(ctx, obj, "active", JS_NewBool(ctx, state.active)); JS_SetPropertyStr(ctx, obj, "reason", JS_NewString(ctx, state.reason[0] ? state.reason : "unknown")); JS_SetPropertyStr(ctx, obj, "udc", JS_NewString(ctx, state.udc)); JS_SetPropertyStr(ctx, obj, "port", JS_NewString(ctx, state.port)); JS_SetPropertyStr(ctx, obj, "powerRole", JS_NewString(ctx, state.power_role)); JS_SetPropertyStr(ctx, obj, "dataRole", JS_NewString(ctx, state.data_role)); JS_SetPropertyStr(ctx, obj, "alsaDevice", JS_NewString(ctx, state.alsa_device)); JS_SetPropertyStr(ctx, obj, "serial", JS_NewString(ctx, state.serial)); return obj;}
static JSValue js_usb_midi_status(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; return build_usb_midi_state_obj(ctx);}
static JSValue js_usb_midi_control(JSContext *ctx, const char *action) { usb_midi_close(); if (access("/scripts/usb-midi-gadget.sh", X_OK) == 0) { char cmd[256]; snprintf(cmd, sizeof(cmd), "/scripts/usb-midi-gadget.sh %s >/tmp/usb-midi-gadget.log 2>&1", action); int rc = system(cmd); ac_log("[usb-midi] control %s rc=%d", action, rc); } else { FILE *fp = fopen("/run/usb-midi.state", "w"); if (fp) { fputs("enabled=0\nactive=0\nreason=missing-script\n", fp); fclose(fp); } ac_log("[usb-midi] control %s failed: missing /scripts/usb-midi-gadget.sh", action); } usb_midi_close(); return build_usb_midi_state_obj(ctx);}
static JSValue js_usb_midi_enable(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; return js_usb_midi_control(ctx, "up");}
static JSValue js_usb_midi_disable(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; return js_usb_midi_control(ctx, "down");}
static JSValue js_usb_midi_refresh(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; return js_usb_midi_control(ctx, "refresh");}
static JSValue js_usb_midi_note_on(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; int32_t note = 60; int32_t velocity = 100; int32_t channel = 0; if (argc < 1 || JS_ToInt32(ctx, ¬e, argv[0])) return JS_FALSE; if (argc >= 2) JS_ToInt32(ctx, &velocity, argv[1]); if (argc >= 3) JS_ToInt32(ctx, &channel, argv[2]); return JS_NewBool(ctx, usb_midi_note_on(note, velocity, channel));}
static JSValue js_usb_midi_note_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; int32_t note = 60; int32_t velocity = 0; int32_t channel = 0; if (argc < 1 || JS_ToInt32(ctx, ¬e, argv[0])) return JS_FALSE; if (argc >= 2) JS_ToInt32(ctx, &velocity, argv[1]); if (argc >= 3) JS_ToInt32(ctx, &channel, argv[2]); return JS_NewBool(ctx, usb_midi_note_off(note, velocity, channel));}
static JSValue js_usb_midi_all_notes_off(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; int32_t channel = 0; if (argc >= 1) JS_ToInt32(ctx, &channel, argv[0]); return JS_NewBool(ctx, usb_midi_all_notes_off(channel));}
// Hardened: triple sync with delays, pre-reboot EFI file size sanity check,// perf flush, log flush before rebooting.static JSValue js_reboot(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)ctx; (void)this_val; (void)argc; (void)argv; ac_log("[system] reboot requested");
// Pre-reboot sanity: verify EFI file exists and is >1MB (catch corrupted flash) { const char *efi_paths[] = { "/mnt/EFI/BOOT/BOOTX64.EFI", "/tmp/efi/EFI/BOOT/BOOTX64.EFI", NULL }; int efi_ok = 0; for (int i = 0; efi_paths[i]; i++) { struct stat st; if (stat(efi_paths[i], &st) == 0 && st.st_size > 1048576) { ac_log("[reboot] EFI verified: %s (%ld bytes)", efi_paths[i], (long)st.st_size); efi_ok = 1; break; } } if (!efi_ok) { ac_log("[reboot] WARNING: no valid EFI file found on mount — rebooting anyway"); } }
// Flush perf data before reboot perf_flush();
// Play shutdown chime and let audio drain if (current_rt && current_rt->audio) { audio_shutdown_sound(current_rt->audio); usleep(500000); // 500ms for chime to play }
// Flush log file ac_log("[reboot] syncing filesystems..."); ac_log_flush();
// Triple sync with delays — vfat write-back needs time sync(); usleep(500000); sync(); usleep(500000); sync();
ac_log("[reboot] executing reboot syscall"); ac_log_flush();
// Exit with code 2 — init script sees this as reboot request _exit(2); return JS_UNDEFINED;}
// system.poweroff() — signal main loop to run shutdown animation then exitextern volatile int poweroff_requested;static JSValue js_poweroff(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)ctx; (void)this_val; (void)argc; (void)argv; ac_log("[system] poweroff requested via JS"); poweroff_requested = 1; // main loop will run bye animation + exit(0) return JS_UNDEFINED;}
// ============================================================// JS Native Functions — PTY Terminal Emulator// ============================================================
// system.pty.spawn(cmd, [args...], cols, rows) — spawn a process in a PTYstatic JSValue js_pty_spawn(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 1) return JS_FALSE; if (current_rt->pty_active) { pty_destroy(¤t_rt->pty); current_rt->pty_active = 0; }
const char *cmd = JS_ToCString(ctx, argv[0]); if (!cmd) return JS_FALSE;
// Collect args from JS array (argv[1]) or use cmd as argv[0] char *child_argv[32] = {0}; int nargs = 0; child_argv[nargs++] = (char *)cmd;
if (argc > 1 && JS_IsArray(ctx, argv[1])) { int len = 0; JSValue jlen = JS_GetPropertyStr(ctx, argv[1], "length"); JS_ToInt32(ctx, &len, jlen); JS_FreeValue(ctx, jlen); for (int i = 0; i < len && nargs < 30; i++) { JSValue el = JS_GetPropertyUint32(ctx, argv[1], i); child_argv[nargs++] = (char *)JS_ToCString(ctx, el); JS_FreeValue(ctx, el); } } child_argv[nargs] = NULL;
int cols = 80, rows = 24; if (argc > 2) JS_ToInt32(ctx, &cols, argv[2]); if (argc > 3) JS_ToInt32(ctx, &rows, argv[3]);
int ok = pty_spawn(¤t_rt->pty, cols, rows, cmd, child_argv);
// Free ToCString results for args for (int i = 1; i < nargs; i++) { JS_FreeCString(ctx, child_argv[i]); } JS_FreeCString(ctx, cmd);
if (ok == 0) { current_rt->pty_active = 1; return JS_TRUE; } return JS_FALSE;}
// system.pty.write(str) — send input to PTYstatic JSValue js_pty_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->pty_active || argc < 1) return JS_UNDEFINED; size_t len; const char *data = JS_ToCStringLen(ctx, &len, argv[0]); if (!data) return JS_UNDEFINED; pty_write(¤t_rt->pty, data, (int)len); JS_FreeCString(ctx, data); return JS_UNDEFINED;}
// system.pty.resize(cols, rows) — resize PTYstatic JSValue js_pty_resize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->pty_active || argc < 2) return JS_UNDEFINED; int cols, rows; JS_ToInt32(ctx, &cols, argv[0]); JS_ToInt32(ctx, &rows, argv[1]); pty_resize(¤t_rt->pty, cols, rows); return JS_UNDEFINED;}
// system.pty.kill() — kill the PTY childstatic JSValue js_pty_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt || !current_rt->pty_active) return JS_UNDEFINED; pty_destroy(¤t_rt->pty); current_rt->pty_active = 0; return JS_UNDEFINED;}
// JS Native Functions — PTY2 (second terminal for split-screen)// ============================================================
static JSValue js_pty2_spawn(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 1) return JS_FALSE; if (current_rt->pty2_active) { pty_destroy(¤t_rt->pty2); current_rt->pty2_active = 0; } const char *cmd = JS_ToCString(ctx, argv[0]); if (!cmd) return JS_FALSE; char *child_argv[32] = {0}; int nargs = 0; child_argv[nargs++] = (char *)cmd; if (argc > 1 && JS_IsArray(ctx, argv[1])) { int len = 0; JSValue jlen = JS_GetPropertyStr(ctx, argv[1], "length"); JS_ToInt32(ctx, &len, jlen); JS_FreeValue(ctx, jlen); for (int i = 0; i < len && nargs < 30; i++) { JSValue el = JS_GetPropertyUint32(ctx, argv[1], i); child_argv[nargs++] = (char *)JS_ToCString(ctx, el); JS_FreeValue(ctx, el); } } child_argv[nargs] = NULL; int cols = 80, rows = 24; if (argc > 2) JS_ToInt32(ctx, &cols, argv[2]); if (argc > 3) JS_ToInt32(ctx, &rows, argv[3]); int ok = pty_spawn(¤t_rt->pty2, cols, rows, cmd, child_argv); for (int i = 1; i < nargs; i++) JS_FreeCString(ctx, child_argv[i]); JS_FreeCString(ctx, cmd); if (ok == 0) { current_rt->pty2_active = 1; return JS_TRUE; } return JS_FALSE;}
static JSValue js_pty2_write(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->pty2_active || argc < 1) return JS_UNDEFINED; size_t len; const char *data = JS_ToCStringLen(ctx, &len, argv[0]); if (!data) return JS_UNDEFINED; pty_write(¤t_rt->pty2, data, (int)len); JS_FreeCString(ctx, data); return JS_UNDEFINED;}
static JSValue js_pty2_resize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->pty2_active || argc < 2) return JS_UNDEFINED; int cols, rows; JS_ToInt32(ctx, &cols, argv[0]); JS_ToInt32(ctx, &rows, argv[1]); pty_resize(¤t_rt->pty2, cols, rows); return JS_UNDEFINED;}
static JSValue js_pty2_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (!current_rt || !current_rt->pty2_active) return JS_UNDEFINED; pty_destroy(¤t_rt->pty2); current_rt->pty2_active = 0; return JS_UNDEFINED;}
// system.jump(pieceName) — request piece switch (handled in main loop)// system.saveConfig(key, value) — merge a key-value pair into /mnt/config.jsonstatic JSValue js_save_config(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2) return JS_FALSE; const char *key = JS_ToCString(ctx, argv[0]); const char *val = JS_ToCString(ctx, argv[1]); if (!key || !val) { JS_FreeCString(ctx, key); JS_FreeCString(ctx, val); return JS_FALSE; }
// Read existing config char buf[8192] = {0}; FILE *f = fopen("/mnt/config.json", "r"); if (f) { size_t n = fread(buf, 1, sizeof(buf) - 1, f); buf[n] = '\0'; fclose(f); }
// Simple JSON merge: if key exists, replace its value; otherwise append // We build a new JSON string manually since we don't have a JSON library char out[8192] = {0}; char needle[256]; snprintf(needle, sizeof(needle), "\"%s\"", key);
// Detect if value is a bare literal (boolean/number) — don't quote it int is_bare = (strcmp(val, "true") == 0 || strcmp(val, "false") == 0 || (val[0] >= '0' && val[0] <= '9') || val[0] == '-'); const char *qL = is_bare ? "" : "\""; const char *qR = is_bare ? "" : "\"";
char *existing = strstr(buf, needle); if (existing && buf[0] == '{') { // Key exists — find value start (after ":") and end (next , or }) char *colon = strchr(existing, ':'); if (colon) { char *vstart = colon + 1; while (*vstart == ' ') vstart++; char *vend; if (*vstart == '"') { vend = strchr(vstart + 1, '"'); if (vend) vend++; // past closing quote } else { vend = vstart; while (*vend && *vend != ',' && *vend != '}') vend++; } if (vend) { int prefix_len = (int)(vstart - buf); snprintf(out, sizeof(out), "%.*s%s%s%s%s", prefix_len, buf, qL, val, qR, vend); } } } else if (buf[0] == '{') { // Append new key before closing } char *closing = strrchr(buf, '}'); if (closing) { int prefix_len = (int)(closing - buf); // Check if there's existing content (non-empty object) int has_content = 0; for (char *p = buf + 1; p < closing; p++) { if (*p != ' ' && *p != '\n' && *p != '\r' && *p != '\t') { has_content = 1; break; } } snprintf(out, sizeof(out), "%.*s%s\"%s\":%s%s%s}", prefix_len, buf, has_content ? "," : "", key, qL, val, qR); } } else { // No valid JSON — create new snprintf(out, sizeof(out), "{\"%s\":%s%s%s}", key, qL, val, qR); }
f = fopen("/mnt/config.json", "w"); if (f) { fputs(out[0] ? out : buf, f); fflush(f); fsync(fileno(f)); fclose(f); config_cache_dirty = 1; ac_log("[config] saved %s to /mnt/config.json\n", key); // Update runtime config if it's a known field if (current_rt && strcmp(key, "handle") == 0) { strncpy(current_rt->handle, val, sizeof(current_rt->handle) - 1); } else if (current_rt && strcmp(key, "piece") == 0) { strncpy(current_rt->piece, val, sizeof(current_rt->piece) - 1); } else if (strcmp(key, "voice") == 0) { extern int voice_off; voice_off = (strcmp(val, "off") == 0); ac_log("[config] voice_off = %d\n", voice_off); } } else { ac_log("[config] ERROR: cannot write /mnt/config.json\n"); }
JS_FreeCString(ctx, key); JS_FreeCString(ctx, val); return JS_TRUE;}
// system.startSSH() — start dropbear SSH daemon (generates host key if needed)static int ssh_started = 0;static JSValue js_start_ssh(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (ssh_started) return JS_NewBool(ctx, 1);
// Check if dropbear exists if (access("/usr/sbin/dropbear", X_OK) != 0 && access("/bin/dropbear", X_OK) != 0) { ac_log("[ssh] dropbear not found in initramfs\n"); return JS_NewBool(ctx, 0); }
// Host key: restore from the EFI partition so the device keeps a stable // SSH identity across reboots (initramfs is volatile); generate + persist // on first boot. if (access("/etc/dropbear/dropbear_ed25519_host_key", F_OK) != 0) { ac_log("[ssh] preparing host key...\n"); system("mkdir -p /etc/dropbear && " "{ cp /mnt/.dropbear_ed25519_host_key /etc/dropbear/dropbear_ed25519_host_key 2>/dev/null || " "{ dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key 2>/dev/null && " "cp /etc/dropbear/dropbear_ed25519_host_key /mnt/.dropbear_ed25519_host_key 2>/dev/null; }; }"); }
// Key-only auth when an authorized_keys file is baked in (public OTA // builds — root's home is /root per /etc/passwd, so dropbear reads // /root/.ssh/authorized_keys). -B (blank-password root) only as a // fallback for local dev images without baked keys. int have_keys = (access("/root/.ssh/authorized_keys", F_OK) == 0); ac_log("[ssh] starting dropbear on port 22 (authorized_keys=%s)...\n", have_keys ? "/root/.ssh" : "none -> -B fallback"); if (have_keys) { system("dropbear -R -s -g -p 22 -P /tmp/dropbear.pid 2>/tmp/dropbear.log &"); } else { system("dropbear -R -B -p 22 -P /tmp/dropbear.pid 2>/tmp/dropbear.log &"); } ssh_started = 1; ac_log("[ssh] dropbear started\n"); return JS_NewBool(ctx, 1);}
// system.execNode(script) — run Node.js with a script string, asyncstatic JSValue js_exec_node(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 1) return JS_UNDEFINED; if (current_rt->fetch_pending) return JS_FALSE; // reuse fetch slot for output
const char *script = JS_ToCString(ctx, argv[0]); if (!script) return JS_UNDEFINED;
// Write script to temp file FILE *sf = fopen("/tmp/ac_node_script.mjs", "w"); if (sf) { fputs(script, sf); fclose(sf); }
ac_log("[node] executing script (%ld bytes)\n", (long)strlen(script)); // Run node and capture output to /tmp/ac_fetch.json (reusing fetch result slot) unlink("/tmp/ac_fetch.json"); unlink("/tmp/ac_fetch_rc"); unlink("/tmp/ac_fetch_err"); system("sh -c 'node /tmp/ac_node_script.mjs > /tmp/ac_fetch.json 2>/tmp/ac_fetch_err;" " echo $? > /tmp/ac_fetch_rc' &"); current_rt->fetch_pending = 1; current_rt->fetch_result[0] = 0; current_rt->fetch_error[0] = 0;
JS_FreeCString(ctx, script); return JS_TRUE;}
// system.listPieces() — scan /pieces/*.mjs and /pieces/*.lisp, return array of piece namesstatic JSValue js_list_pieces(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; JSValue arr = JS_NewArray(ctx); DIR *d = opendir("/pieces"); if (d) { struct dirent *ent; uint32_t idx = 0; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; // Check for .mjs char *dot = strstr(ent->d_name, ".mjs"); if (dot && dot[4] == '\0') { char name[64]; int len = (int)(dot - ent->d_name); if (len > 63) len = 63; memcpy(name, ent->d_name, len); name[len] = '\0'; JS_SetPropertyUint32(ctx, arr, idx++, JS_NewString(ctx, name)); continue; } // Check for .lisp — include extension so list.mjs can distinguish dot = strstr(ent->d_name, ".lisp"); if (dot && dot[5] == '\0') { JS_SetPropertyUint32(ctx, arr, idx++, JS_NewString(ctx, ent->d_name)); } } closedir(d); } return arr;}
// system.listPrinters() — detect USB printers via /dev/usb/lp* and sysfsstatic JSValue js_list_printers(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; JSValue arr = JS_NewArray(ctx); uint32_t idx = 0;
// Scan /dev/usb/lp* devices DIR *d = opendir("/dev/usb"); if (d) { struct dirent *ent; while ((ent = readdir(d)) != NULL) { if (strncmp(ent->d_name, "lp", 2) != 0) continue; char devpath[64]; snprintf(devpath, sizeof(devpath), "/dev/usb/%s", ent->d_name);
JSValue printer = JS_NewObject(ctx); JS_SetPropertyStr(ctx, printer, "device", JS_NewString(ctx, devpath)); JS_SetPropertyStr(ctx, printer, "id", JS_NewString(ctx, ent->d_name));
// Try to find printer name via sysfs usbmisc char syspath[256], name[128] = {0}, vendor[128] = {0}; snprintf(syspath, sizeof(syspath), "/sys/class/usbmisc/%s/device/../product", ent->d_name); FILE *fp = fopen(syspath, "r"); if (fp) { if (fgets(name, sizeof(name), fp)) { char *nl = strchr(name, '\n'); if (nl) *nl = 0; } fclose(fp); } snprintf(syspath, sizeof(syspath), "/sys/class/usbmisc/%s/device/../manufacturer", ent->d_name); fp = fopen(syspath, "r"); if (fp) { if (fgets(vendor, sizeof(vendor), fp)) { char *nl = strchr(vendor, '\n'); if (nl) *nl = 0; } fclose(fp); }
if (name[0]) JS_SetPropertyStr(ctx, printer, "name", JS_NewString(ctx, name)); else JS_SetPropertyStr(ctx, printer, "name", JS_NewString(ctx, ent->d_name)); if (vendor[0]) JS_SetPropertyStr(ctx, printer, "vendor", JS_NewString(ctx, vendor));
JS_SetPropertyUint32(ctx, arr, idx++, printer); } closedir(d); } return arr;}
// system.printRaw(devicePath, byteArray) — write raw bytes to printer device// byteArray is a JS array of integers (0-255)static JSValue js_print_raw(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 2) return JS_FALSE; const char *devpath = JS_ToCString(ctx, argv[0]); if (!devpath) return JS_FALSE;
// Validate device path starts with /dev/usb/lp if (strncmp(devpath, "/dev/usb/lp", 11) != 0) { ac_log("[print] rejected non-printer path: %s\n", devpath); JS_FreeCString(ctx, devpath); return JS_FALSE; }
// Get array length JSValue lenVal = JS_GetPropertyStr(ctx, argv[1], "length"); uint32_t len = 0; JS_ToUint32(ctx, &len, lenVal); JS_FreeValue(ctx, lenVal);
if (len == 0 || len > 65536) { ac_log("[print] invalid data length: %u\n", len); JS_FreeCString(ctx, devpath); return JS_FALSE; }
// Build byte buffer from JS array uint8_t *buf = malloc(len); if (!buf) { JS_FreeCString(ctx, devpath); return JS_FALSE; } for (uint32_t i = 0; i < len; i++) { JSValue v = JS_GetPropertyUint32(ctx, argv[1], i); int32_t b = 0; JS_ToInt32(ctx, &b, v); JS_FreeValue(ctx, v); buf[i] = (uint8_t)(b & 0xFF); }
// Open device and write FILE *fp = fopen(devpath, "wb"); if (!fp) { ac_log("[print] failed to open %s: %s\n", devpath, strerror(errno)); free(buf); JS_FreeCString(ctx, devpath); return JS_FALSE; }
size_t written = fwrite(buf, 1, len, fp); fflush(fp); fclose(fp); free(buf);
ac_log("[print] wrote %zu/%u bytes to %s\n", written, len, devpath); JS_FreeCString(ctx, devpath); return written == len ? JS_TRUE : JS_FALSE;}
// system.dmxSend(channels) — push one DMX universe frame to the first USB DMX// widget (Enttec Pro framing — DMXking USB PRO et al; the widget owns DMX// timing, we just hand it slots). fd persists across calls; a failed write// closes it so unplug/replug self-heals on the next send.static int dmx_fd = -1;
static int dmx_open(void) { if (dmx_fd >= 0) return dmx_fd; for (int i = 0; i < 4; i++) { char path[24]; snprintf(path, sizeof(path), "/dev/ttyUSB%d", i); int fd = open(path, O_WRONLY | O_NOCTTY); if (fd < 0) continue; struct termios t; if (tcgetattr(fd, &t) == 0) { cfmakeraw(&t); // raw — the frame bytes must pass untouched cfsetospeed(&t, B115200); cfsetispeed(&t, B115200); tcsetattr(fd, TCSANOW, &t); } ac_log("💡 [dmx] opened %s\n", path); dmx_fd = fd; return fd; } return -1;}
static JSValue js_dmx_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_FALSE; int fd = dmx_open(); if (fd < 0) return JS_FALSE;
JSValue lenVal = JS_GetPropertyStr(ctx, argv[0], "length"); uint32_t len = 0; JS_ToUint32(ctx, &len, lenVal); JS_FreeValue(ctx, lenVal); if (len > 512) len = 512; uint32_t slots = len < 24 ? 24 : len; // widget wants ≥24 slots per frame
// 7E 06 lenLo lenHi | 00 startcode + slots | E7 uint8_t pkt[5 + 1 + 512 + 1]; uint16_t payload = (uint16_t)(slots + 1); pkt[0] = 0x7E; pkt[1] = 0x06; pkt[2] = payload & 0xFF; pkt[3] = payload >> 8; pkt[4] = 0x00; // DMX start code memset(pkt + 5, 0, slots); for (uint32_t i = 0; i < len; i++) { JSValue v = JS_GetPropertyUint32(ctx, argv[0], i); int32_t b = 0; JS_ToInt32(ctx, &b, v); JS_FreeValue(ctx, v); pkt[5 + i] = (uint8_t)(b & 0xFF); } pkt[5 + slots] = 0xE7;
ssize_t n = write(fd, pkt, 5 + slots + 1); if (n != (ssize_t)(5 + slots + 1)) { ac_log("💡 [dmx] write failed (%s) — closing\n", strerror(errno)); close(dmx_fd); dmx_fd = -1; return JS_FALSE; } return JS_TRUE;}
static JSValue js_jump(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 1) return JS_UNDEFINED; const char *name = JS_ToCString(ctx, argv[0]); if (!name) return JS_UNDEFINED; strncpy(current_rt->jump_target, name, sizeof(current_rt->jump_target) - 1); current_rt->jump_target[sizeof(current_rt->jump_target) - 1] = 0; current_rt->jump_requested = 1; ac_log("[system] jump requested: %s", name); JS_FreeCString(ctx, name); return JS_UNDEFINED;}
// system.volumeAdjust(delta) — +1 up, -1 down, 0 mute togglestatic JSValue js_volume_adjust(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || !current_rt->audio || argc < 1) return JS_UNDEFINED; int delta = 0; JS_ToInt32(ctx, &delta, argv[0]); audio_volume_adjust(current_rt->audio, delta); return JS_UNDEFINED;}
// system.brightnessAdjust(delta) — +1 up, -1 downstatic JSValue js_brightness_adjust(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_UNDEFINED; int delta = 0; JS_ToInt32(ctx, &delta, argv[0]); // Read current backlight from sysfs DIR *bldir = opendir("/sys/class/backlight"); if (!bldir) return JS_UNDEFINED; struct dirent *ent; while ((ent = readdir(bldir))) { if (ent->d_name[0] == '.') continue; char tmp[160]; snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/max_brightness", ent->d_name); FILE *f = fopen(tmp, "r"); if (!f) continue; int bl_max = 100; fscanf(f, "%d", &bl_max); fclose(f); snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/brightness", ent->d_name); f = fopen(tmp, "r"); int cur = bl_max / 2; if (f) { fscanf(f, "%d", &cur); fclose(f); } int step = bl_max / 20; // 5% if (step < 1) step = 1; cur += delta * step; if (cur < 1) cur = 1; if (cur > bl_max) cur = bl_max; f = fopen(tmp, "w"); if (f) { fprintf(f, "%d", cur); fclose(f); } break; } closedir(bldir); return JS_UNDEFINED;}
// system.openBrowser(url) — launch Firefox for OAuth login// Under Wayland: fork+exec firefox as sibling Wayland client (cage composites it on top)// Under DRM: releases DRM master, runs cage+firefox, reclaims on exit// Returns true if browser exited normally.static JSValue js_open_browser(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt) return JS_FALSE; const char *url = JS_ToCString(ctx, argv[0]); if (!url) return JS_FALSE;
ac_log("[browser] Opening: %s", url); int rc = -1;
#ifdef USE_WAYLAND if (getenv("WAYLAND_DISPLAY")) { // Under cage: just fork+exec Firefox as a sibling Wayland client // cage handles window stacking — Firefox appears on top, ac-native resumes when it exits pid_t pid = fork(); if (pid == 0) { // Child process — exec firefox setenv("MOZ_ENABLE_WAYLAND", "1", 1); setenv("GDK_BACKEND", "wayland", 1); setenv("MOZ_DISABLE_CONTENT_SANDBOX", "1", 1); setenv("DBUS_SESSION_BUS_ADDRESS", "disabled:", 1); setenv("MOZ_DBUS_REMOTE", "0", 1); setenv("HOME", "/tmp", 1); setenv("LD_LIBRARY_PATH", "/lib64:/opt/firefox", 1); setenv("GRE_HOME", "/opt/firefox", 1); mkdir("/tmp/.mozilla", 0700); execlp("/opt/firefox/firefox-bin", "firefox-bin", "--kiosk", "--no-remote", "--new-instance", url, NULL); _exit(127); } else if (pid > 0) { // Parent — wait for Firefox to exit int status; waitpid(pid, &status, 0); rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1; ac_log("[browser] Firefox exited: %d", rc); } else { ac_log("[browser] fork failed: %s", strerror(errno)); } } else#endif { // Legacy DRM handoff path extern int drm_release_master(void *display); extern int drm_acquire_master(void *display); extern void *g_display; if (g_display) { drm_release_master(g_display); ac_log("[browser] Released DRM master"); }
char cmd[4096]; mkdir("/tmp/xdg", 0700); mkdir("/tmp/.mozilla", 0700); snprintf(cmd, sizeof(cmd), "export HOME=/tmp && " "export XDG_RUNTIME_DIR=/tmp/xdg && " "export WLR_BACKENDS=drm && " "export WLR_SESSION=direct && " "export WLR_RENDERER=pixman && " "export LIBSEAT_BACKEND=noop && " "export LD_LIBRARY_PATH=/lib64:/opt/firefox && " "export LIBGL_ALWAYS_SOFTWARE=1 && " "export MOZ_ENABLE_WAYLAND=1 && " "export GDK_BACKEND=wayland && " "cage -s -- /opt/firefox/firefox-bin " "--kiosk --no-remote --new-instance '%s' " ">/mnt/cage.log 2>&1; echo \"exit=$?\" >>/mnt/cage.log", url); rc = system(cmd); ac_log("[browser] cage exited: %d", rc);
if (g_display) { drm_acquire_master(g_display); ac_log("[browser] Reclaimed DRM master"); } }
JS_FreeCString(ctx, url); return JS_NewBool(ctx, rc == 0);}
// system.qrEncode(text) → { size: N, modules: [bool, bool, ...] }// Encodes text as QR code using nayuki qrcodegen, returns module grid.static JSValue js_qr_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1) return JS_UNDEFINED; const char *text = JS_ToCString(ctx, argv[0]); if (!text) return JS_UNDEFINED;
// Buffers for qrcodegen (version 1-40, up to ~4296 chars at ECC LOW) uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX]; uint8_t tempBuf[qrcodegen_BUFFER_LEN_MAX];
bool ok = qrcodegen_encodeText(text, tempBuf, qrcode, qrcodegen_Ecc_LOW, qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true); JS_FreeCString(ctx, text);
if (!ok) return JS_UNDEFINED;
int size = qrcodegen_getSize(qrcode); JSValue result = JS_NewObject(ctx); JS_SetPropertyStr(ctx, result, "size", JS_NewInt32(ctx, size));
JSValue modules = JS_NewArray(ctx); for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { JS_SetPropertyUint32(ctx, modules, (uint32_t)(y * size + x), JS_NewBool(ctx, qrcodegen_getModule(qrcode, x, y))); } } JS_SetPropertyStr(ctx, result, "modules", modules); return result;}
// nopaint.is(stateStr) — returns true if current nopaint state matchesstatic JSValue js_nopaint_is(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (argc < 1 || !current_rt) return JS_FALSE; const char *query = JS_ToCString(ctx, argv[0]); if (!query) return JS_FALSE; int match = 0; if (strcmp(query, "painting") == 0) match = (current_rt->nopaint_state == 1); else if (strcmp(query, "idle") == 0) match = (current_rt->nopaint_state == 0); JS_FreeCString(ctx, query); return JS_NewBool(ctx, match);}
// nopaint.cancelStroke() — abort current strokestatic JSValue js_nopaint_cancel(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; (void)argc; (void)argv; if (current_rt) { current_rt->nopaint_state = 0; current_rt->nopaint_needs_bake = 0; } return JS_UNDEFINED;}
static JSValue build_system_obj(JSContext *ctx) { JSValue sys = JS_NewObject(ctx);
// Battery info — scan /sys/class/power_supply/ for any battery char buf[64]; int capacity = -1, power_now = 0, energy_now = 0; const char *status = "Unknown"; char bat_path[128] = "";
// Try BAT0, BAT1, or scan directory const char *bat_names[] = {"BAT0", "BAT1", NULL}; for (int i = 0; bat_names[i] && !bat_path[0]; i++) { char tmp[128]; snprintf(tmp, sizeof(tmp), "/sys/class/power_supply/%s/capacity", bat_names[i]); if (read_sysfs(tmp, buf, sizeof(buf)) > 0) { snprintf(bat_path, sizeof(bat_path), "/sys/class/power_supply/%s", bat_names[i]); } } // Fallback: scan directory if (!bat_path[0]) { DIR *dir = opendir("/sys/class/power_supply"); if (dir) { struct dirent *ent; while ((ent = readdir(dir)) && !bat_path[0]) { if (ent->d_name[0] == '.') continue; char tmp[160]; snprintf(tmp, sizeof(tmp), "/sys/class/power_supply/%s/type", ent->d_name); if (read_sysfs(tmp, buf, sizeof(buf)) > 0 && strcmp(buf, "Battery") == 0) { snprintf(bat_path, sizeof(bat_path), "/sys/class/power_supply/%s", ent->d_name); } } closedir(dir); } }
if (bat_path[0]) { char tmp[160]; snprintf(tmp, sizeof(tmp), "%s/capacity", bat_path); if (read_sysfs(tmp, buf, sizeof(buf)) > 0) capacity = atoi(buf); snprintf(tmp, sizeof(tmp), "%s/status", bat_path); if (read_sysfs(tmp, buf, sizeof(buf)) > 0) status = (strcmp(buf, "Charging") == 0) ? "Charging" : (strcmp(buf, "Full") == 0) ? "Full" : (strcmp(buf, "Discharging") == 0) ? "Discharging" : "Unknown"; snprintf(tmp, sizeof(tmp), "%s/power_now", bat_path); if (read_sysfs(tmp, buf, sizeof(buf)) > 0) power_now = atoi(buf); snprintf(tmp, sizeof(tmp), "%s/energy_now", bat_path); if (read_sysfs(tmp, buf, sizeof(buf)) > 0) energy_now = atoi(buf); }
JSValue battery = JS_NewObject(ctx); JS_SetPropertyStr(ctx, battery, "percent", JS_NewInt32(ctx, capacity)); JS_SetPropertyStr(ctx, battery, "charging", JS_NewBool(ctx, strcmp(status, "Charging") == 0)); JS_SetPropertyStr(ctx, battery, "status", JS_NewString(ctx, status));
if (power_now > 0 && energy_now > 0 && strcmp(status, "Discharging") == 0) { double hours = (double)energy_now / (double)power_now; JS_SetPropertyStr(ctx, battery, "minutesLeft", JS_NewInt32(ctx, (int)(hours * 60))); } else { JS_SetPropertyStr(ctx, battery, "minutesLeft", JS_NewInt32(ctx, -1)); }
JS_SetPropertyStr(ctx, sys, "battery", battery);
// Hardware info — system.hw (model, cpu, ram) { JSValue hw = JS_NewObject(ctx); char hbuf[256];
// Product name: /sys/class/dmi/id/product_name if (read_sysfs("/sys/class/dmi/id/product_name", hbuf, sizeof(hbuf)) > 0) JS_SetPropertyStr(ctx, hw, "model", JS_NewString(ctx, hbuf)); else JS_SetPropertyStr(ctx, hw, "model", JS_NewString(ctx, "unknown"));
// Vendor: /sys/class/dmi/id/sys_vendor if (read_sysfs("/sys/class/dmi/id/sys_vendor", hbuf, sizeof(hbuf)) > 0) JS_SetPropertyStr(ctx, hw, "vendor", JS_NewString(ctx, hbuf)); else JS_SetPropertyStr(ctx, hw, "vendor", JS_NewString(ctx, "unknown"));
// CPU model and core count, read once. /proc/cpuinfo was opened and // scanned line-by-line TWICE on every build_api call — three times a // frame — to recover two facts that cannot change while the machine is // running. Statics, filled on first use. { static char cpuline[256]; static int cores = -1; if (cores < 0) { cores = 0; FILE *cpuinfo = fopen("/proc/cpuinfo", "r"); if (cpuinfo) { char line[512]; while (fgets(line, sizeof(line), cpuinfo)) { if (!cpuline[0] && strncmp(line, "model name", 10) == 0) { char *colon = strchr(line, ':'); if (colon) { colon++; while (*colon == ' ') colon++; char *nl = strchr(colon, '\n'); if (nl) *nl = 0; strncpy(cpuline, colon, sizeof(cpuline) - 1); } } // Both facts now come from a single pass. if (strncmp(line, "processor", 9) == 0) cores++; } fclose(cpuinfo); } } JS_SetPropertyStr(ctx, hw, "cpu", JS_NewString(ctx, cpuline[0] ? cpuline : "unknown")); JS_SetPropertyStr(ctx, hw, "cores", JS_NewInt32(ctx, cores)); }
// RAM: total from /proc/meminfo (in MB) { FILE *meminfo = fopen("/proc/meminfo", "r"); long total_kb = 0, avail_kb = 0; if (meminfo) { char line[256]; while (fgets(line, sizeof(line), meminfo)) { if (strncmp(line, "MemTotal:", 9) == 0) { sscanf(line + 9, " %ld", &total_kb); } else if (strncmp(line, "MemAvailable:", 13) == 0) { sscanf(line + 13, " %ld", &avail_kb); } if (total_kb && avail_kb) break; } fclose(meminfo); } JS_SetPropertyStr(ctx, hw, "ramTotalMB", JS_NewInt32(ctx, (int)(total_kb / 1024))); JS_SetPropertyStr(ctx, hw, "ramAvailMB", JS_NewInt32(ctx, (int)(avail_kb / 1024))); }
// Process count from /proc (count numeric dirs) { int procs = 0; DIR *pd = opendir("/proc"); if (pd) { struct dirent *pe; while ((pe = readdir(pd))) { if (pe->d_name[0] >= '1' && pe->d_name[0] <= '9') procs++; } closedir(pd); } JS_SetPropertyStr(ctx, hw, "processes", JS_NewInt32(ctx, procs)); }
// Load average from /proc/loadavg { char labuf[128] = {0}; FILE *la = fopen("/proc/loadavg", "r"); if (la) { if (fgets(labuf, sizeof(labuf), la)) { // "0.12 0.34 0.56 1/42 1234" double l1 = 0, l5 = 0, l15 = 0; sscanf(labuf, "%lf %lf %lf", &l1, &l5, &l15); JS_SetPropertyStr(ctx, hw, "load1", JS_NewFloat64(ctx, l1)); JS_SetPropertyStr(ctx, hw, "load5", JS_NewFloat64(ctx, l5)); JS_SetPropertyStr(ctx, hw, "load15", JS_NewFloat64(ctx, l15)); } fclose(la); } }
// CPU governor (power mode) if (read_sysfs("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", hbuf, sizeof(hbuf)) > 0) { JS_SetPropertyStr(ctx, hw, "governor", JS_NewString(ctx, hbuf)); }
// Connected devices: scan /sys/class and /sys/bus for peripherals { JSValue devs = JS_NewArray(ctx); int di = 0;
// USB devices: scan /sys/bus/usb/devices/*/product { DIR *ud = opendir("/sys/bus/usb/devices"); if (ud) { struct dirent *ue; while ((ue = readdir(ud))) { if (ue->d_name[0] == '.') continue; char pp[256], prod[128] = {0}, mfr[128] = {0}; snprintf(pp, sizeof(pp), "/sys/bus/usb/devices/%s/product", ue->d_name); int got_prod = read_sysfs(pp, prod, sizeof(prod)) > 0; if (!got_prod) continue; snprintf(pp, sizeof(pp), "/sys/bus/usb/devices/%s/manufacturer", ue->d_name); read_sysfs(pp, mfr, sizeof(mfr)); JSValue d = JS_NewObject(ctx); JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "usb")); JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, prod)); if (mfr[0]) JS_SetPropertyStr(ctx, d, "vendor", JS_NewString(ctx, mfr)); JS_SetPropertyStr(ctx, d, "id", JS_NewString(ctx, ue->d_name)); JS_SetPropertyUint32(ctx, devs, di++, d); } closedir(ud); } }
// Input (HID) devices: scan /sys/class/input/*/name { DIR *id = opendir("/sys/class/input"); if (id) { struct dirent *ie; while ((ie = readdir(id))) { if (strncmp(ie->d_name, "event", 5) != 0) continue; char np[256], name[128] = {0}; snprintf(np, sizeof(np), "/sys/class/input/%s/device/name", ie->d_name); if (read_sysfs(np, name, sizeof(name)) <= 0) continue; JSValue d = JS_NewObject(ctx); JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "input")); JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, name)); JS_SetPropertyStr(ctx, d, "id", JS_NewString(ctx, ie->d_name)); JS_SetPropertyUint32(ctx, devs, di++, d); } closedir(id); } }
// Video devices (cameras): /sys/class/video4linux/*/name { DIR *vd = opendir("/sys/class/video4linux"); if (vd) { struct dirent *ve; while ((ve = readdir(vd))) { if (ve->d_name[0] == '.') continue; char vp[256], vname[128] = {0}; snprintf(vp, sizeof(vp), "/sys/class/video4linux/%s/name", ve->d_name); if (read_sysfs(vp, vname, sizeof(vname)) <= 0) continue; JSValue d = JS_NewObject(ctx); JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "camera")); JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, vname)); JS_SetPropertyStr(ctx, d, "id", JS_NewString(ctx, ve->d_name)); JS_SetPropertyUint32(ctx, devs, di++, d); } closedir(vd); } }
// Sound cards: /proc/asound/cards { FILE *sc = fopen("/proc/asound/cards", "r"); if (sc) { char sline[256]; while (fgets(sline, sizeof(sline), sc)) { // Lines like " 0 [PCH ]: HDA-Intel - HDA Intel PCH" char *dash = strstr(sline, " - "); if (dash) { dash += 3; char *nl = strchr(dash, '\n'); if (nl) *nl = 0; JSValue d = JS_NewObject(ctx); JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "audio")); JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, dash)); JS_SetPropertyUint32(ctx, devs, di++, d); } } fclose(sc); } }
// Block devices: /sys/block/*/device/model (drives, USB sticks) { DIR *bd = opendir("/sys/block"); if (bd) { struct dirent *be; while ((be = readdir(bd))) { if (be->d_name[0] == '.') continue; if (strncmp(be->d_name, "loop", 4) == 0) continue; if (strncmp(be->d_name, "ram", 3) == 0) continue; char bp[256], model[128] = {0}; snprintf(bp, sizeof(bp), "/sys/block/%s/device/model", be->d_name); read_sysfs(bp, model, sizeof(model)); // Get size char sz[32] = {0}; snprintf(bp, sizeof(bp), "/sys/block/%s/size", be->d_name); read_sysfs(bp, sz, sizeof(sz)); long sectors = sz[0] ? atol(sz) : 0; int gb = (int)(sectors / 2 / 1024 / 1024); // 512-byte sectors -> GB // Removable? char rem[8] = {0}; snprintf(bp, sizeof(bp), "/sys/block/%s/removable", be->d_name); read_sysfs(bp, rem, sizeof(rem)); JSValue d = JS_NewObject(ctx); JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "disk")); JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, model[0] ? model : be->d_name)); JS_SetPropertyStr(ctx, d, "id", JS_NewString(ctx, be->d_name)); JS_SetPropertyStr(ctx, d, "sizeGB", JS_NewInt32(ctx, gb)); JS_SetPropertyStr(ctx, d, "removable", JS_NewBool(ctx, rem[0] == '1')); JS_SetPropertyUint32(ctx, devs, di++, d); } closedir(bd); } }
// DRM connectors (HDMI, DP, etc.): /sys/class/drm/card*-*/status { DIR *dd = opendir("/sys/class/drm"); if (dd) { struct dirent *de; while ((de = readdir(dd))) { if (strncmp(de->d_name, "card", 4) != 0) continue; if (!strchr(de->d_name, '-')) continue; // skip "card0" itself char sp[256], st[32] = {0}; snprintf(sp, sizeof(sp), "/sys/class/drm/%s/status", de->d_name); if (read_sysfs(sp, st, sizeof(st)) <= 0) continue; JSValue d = JS_NewObject(ctx); JS_SetPropertyStr(ctx, d, "type", JS_NewString(ctx, "display")); JS_SetPropertyStr(ctx, d, "name", JS_NewString(ctx, de->d_name)); JS_SetPropertyStr(ctx, d, "connected", JS_NewBool(ctx, strcmp(st, "connected") == 0)); JS_SetPropertyUint32(ctx, devs, di++, d); } closedir(dd); } }
JS_SetPropertyStr(ctx, hw, "devices", devs); }
// Build stamp, from version.c. Always present — version.c substitutes // "dev"/"unknown" — so these no longer need to be conditional. JS_SetPropertyStr(ctx, hw, "buildName", JS_NewString(ctx, ac_build_name)); JS_SetPropertyStr(ctx, hw, "gitHash", JS_NewString(ctx, ac_git_hash)); JS_SetPropertyStr(ctx, hw, "buildTs", JS_NewString(ctx, ac_build_ts));
// Display driver and GPU info { extern void *g_display; if (g_display) { const char *drv = drm_display_driver((ACDisplay *)g_display); JS_SetPropertyStr(ctx, hw, "displayDriver", JS_NewString(ctx, drv)); } else { JS_SetPropertyStr(ctx, hw, "displayDriver", JS_NewString(ctx, "wayland")); } // GPU renderer, read from sysfs once and remembered. This used to // popen() a shell — inside build_api, which runs for act, sim and // paint every frame AND once per input event, so a held key or a // finger dragging across the pad forked a shell and a `cat` dozens // of times in a single frame. The name cannot change while we are // running, so reading it once is both cheaper and no less correct. static char gpu_name[128]; if (!gpu_name[0]) { static const char *const gpu_paths[] = { "/sys/kernel/debug/dri/0/name", "/sys/class/drm/card0/device/label", }; for (size_t gi = 0; gi < sizeof(gpu_paths) / sizeof(gpu_paths[0]) && !gpu_name[0]; gi++) { FILE *gf = fopen(gpu_paths[gi], "r"); if (!gf) continue; if (fgets(gpu_name, sizeof(gpu_name), gf)) { char *nl = strchr(gpu_name, '\n'); if (nl) *nl = '\0'; } fclose(gf); } if (!gpu_name[0]) snprintf(gpu_name, sizeof(gpu_name), "unknown"); } JS_SetPropertyStr(ctx, hw, "gpu", JS_NewString(ctx, gpu_name)); }
// Audio diagnostics if (current_rt->audio) { JS_SetPropertyStr(ctx, hw, "audioDevice", JS_NewString(ctx, current_rt->audio->audio_device)); JS_SetPropertyStr(ctx, hw, "audioStatus", JS_NewString(ctx, current_rt->audio->audio_status)); JS_SetPropertyStr(ctx, hw, "audioRetries", JS_NewInt32(ctx, current_rt->audio->audio_init_retries)); JS_SetPropertyStr(ctx, hw, "audioRate", JS_NewInt32(ctx, (int)current_rt->audio->actual_rate)); } else { JS_SetPropertyStr(ctx, hw, "audioStatus", JS_NewString(ctx, "no audio subsystem")); }
JS_SetPropertyStr(ctx, sys, "hw", hw); }
// Backlight brightness — scan /sys/class/backlight/ int bl_cur = -1, bl_max = -1; { DIR *bldir = opendir("/sys/class/backlight"); if (bldir) { struct dirent *ent; while ((ent = readdir(bldir))) { if (ent->d_name[0] == '.') continue; char tmp[160]; snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/max_brightness", ent->d_name); if (read_sysfs(tmp, buf, sizeof(buf)) > 0) { bl_max = atoi(buf); snprintf(tmp, sizeof(tmp), "/sys/class/backlight/%s/brightness", ent->d_name); if (read_sysfs(tmp, buf, sizeof(buf)) > 0) bl_cur = atoi(buf); break; } } closedir(bldir); } } if (bl_max > 0 && bl_cur >= 0) { JS_SetPropertyStr(ctx, sys, "brightness", JS_NewInt32(ctx, (bl_cur * 100) / bl_max)); } else { JS_SetPropertyStr(ctx, sys, "brightness", JS_NewInt32(ctx, -1)); }
// USB-C Type-C power role — scan /sys/class/typec/port*/power_role { JSValue typec = JS_NewArray(ctx); int ti = 0; DIR *tcdir = opendir("/sys/class/typec"); if (tcdir) { struct dirent *te; while ((te = readdir(tcdir))) { if (strncmp(te->d_name, "port", 4) != 0) continue; // Skip partner/plug entries like "port0-partner" if (strchr(te->d_name + 4, '-')) continue; char tmp[256], rbuf[64] = {0}; snprintf(tmp, sizeof(tmp), "/sys/class/typec/%s/power_role", te->d_name); if (read_sysfs(tmp, rbuf, sizeof(rbuf)) <= 0) continue; // rbuf is like "[source] sink" or "source [sink]" const char *current_role = "unknown"; int can_swap = 0; if (strstr(rbuf, "[source]")) current_role = "source"; else if (strstr(rbuf, "[sink]")) current_role = "sink"; // If both roles appear, swap is supported if (strstr(rbuf, "source") && strstr(rbuf, "sink")) can_swap = 1; // Data role char drbuf[64] = {0}; snprintf(tmp, sizeof(tmp), "/sys/class/typec/%s/data_role", te->d_name); read_sysfs(tmp, drbuf, sizeof(drbuf)); const char *data_role = "unknown"; if (strstr(drbuf, "[host]")) data_role = "host"; else if (strstr(drbuf, "[device]")) data_role = "device";
JSValue port = JS_NewObject(ctx); JS_SetPropertyStr(ctx, port, "port", JS_NewString(ctx, te->d_name)); JS_SetPropertyStr(ctx, port, "powerRole", JS_NewString(ctx, current_role)); JS_SetPropertyStr(ctx, port, "dataRole", JS_NewString(ctx, data_role)); JS_SetPropertyStr(ctx, port, "canSwap", JS_NewBool(ctx, can_swap)); JS_SetPropertyUint32(ctx, typec, ti++, port); } closedir(tcdir); } JS_SetPropertyStr(ctx, sys, "typec", typec); }
// system.setPowerRole(port, role) — swap USB-C power role ("source" or "sink") JS_SetPropertyStr(ctx, sys, "setPowerRole", JS_NewCFunction(ctx, js_set_power_role, "setPowerRole", 2));
// Tablet mode (lid folded back on convertible laptops) JS_SetPropertyStr(ctx, sys, "tabletMode", JS_NewBool(ctx, current_rt && current_rt->input && current_rt->input->tablet_mode));
// HDMI secondary display int has_hdmi = (current_rt && current_rt->hdmi && current_rt->hdmi->active); JS_SetPropertyStr(ctx, sys, "hasHdmi", JS_NewBool(ctx, has_hdmi)); JS_SetPropertyStr(ctx, sys, "hdmi", JS_NewCFunction(ctx, js_hdmi_fill, "hdmi", 3));
// WebSocket client — system.ws JS_SetPropertyStr(ctx, sys, "ws", build_ws_obj(ctx, current_phase));
// Raw UDP fairy co-presence — system.udp JS_SetPropertyStr(ctx, sys, "udp", build_udp_obj(ctx, current_phase));
// Machine identity — system.machineId { extern char g_machine_id[64]; JS_SetPropertyStr(ctx, sys, "machineId", JS_NewString(ctx, g_machine_id[0] ? g_machine_id : "unknown")); }
// Remote reboot / poweroff — system.reboot() / system.poweroff() JS_SetPropertyStr(ctx, sys, "reboot", JS_NewCFunction(ctx, js_reboot, "reboot", 0)); JS_SetPropertyStr(ctx, sys, "poweroff", JS_NewCFunction(ctx, js_poweroff, "poweroff", 0));
// USB MIDI gadget status + control { JSValue usb_midi = build_usb_midi_state_obj(ctx); JS_SetPropertyStr(ctx, usb_midi, "status", JS_NewCFunction(ctx, js_usb_midi_status, "status", 0)); JS_SetPropertyStr(ctx, usb_midi, "enable", JS_NewCFunction(ctx, js_usb_midi_enable, "enable", 0)); JS_SetPropertyStr(ctx, usb_midi, "disable", JS_NewCFunction(ctx, js_usb_midi_disable, "disable", 0)); JS_SetPropertyStr(ctx, usb_midi, "refresh", JS_NewCFunction(ctx, js_usb_midi_refresh, "refresh", 0)); JS_SetPropertyStr(ctx, usb_midi, "noteOn", JS_NewCFunction(ctx, js_usb_midi_note_on, "noteOn", 3)); JS_SetPropertyStr(ctx, usb_midi, "noteOff", JS_NewCFunction(ctx, js_usb_midi_note_off, "noteOff", 3)); JS_SetPropertyStr(ctx, usb_midi, "allNotesOff", JS_NewCFunction(ctx, js_usb_midi_all_notes_off, "allNotesOff", 1)); JS_SetPropertyStr(ctx, sys, "usbMidi", usb_midi); }
// File I/O — system.readFile(path) / system.writeFile(path, data) JS_SetPropertyStr(ctx, sys, "readFile", JS_NewCFunction(ctx, js_read_file, "readFile", 1)); JS_SetPropertyStr(ctx, sys, "readFileBytes", JS_NewCFunction(ctx, js_read_file_bytes, "readFileBytes", 1)); JS_SetPropertyStr(ctx, sys, "writeFile", JS_NewCFunction(ctx, js_write_file, "writeFile", 2)); JS_SetPropertyStr(ctx, sys, "deleteFile",JS_NewCFunction(ctx, js_delete_file,"deleteFile",1)); JS_SetPropertyStr(ctx, sys, "listDir", JS_NewCFunction(ctx, js_list_dir, "listDir", 1)); JS_SetPropertyStr(ctx, sys, "diskInfo", JS_NewCFunction(ctx, js_disk_info, "diskInfo", 1)); JS_SetPropertyStr(ctx, sys, "blockDevices", JS_NewCFunction(ctx, js_block_devices, "blockDevices", 0)); JS_SetPropertyStr(ctx, sys, "mountMusic", JS_NewCFunction(ctx, js_mount_music, "mountMusic", 0)); JS_SetPropertyStr(ctx, sys, "mountMusicMounted", JS_NewBool(ctx, music_mount_state)); JS_SetPropertyStr(ctx, sys, "mountMusicPending", JS_NewBool(ctx, music_mount_pending));
// Async HTTP fetch — system.fetch(url) / system.fetchCancel() / system.fetchResult / system.fetchPending JS_SetPropertyStr(ctx, sys, "fetch", JS_NewCFunction(ctx, js_fetch, "fetch", 1)); JS_SetPropertyStr(ctx, sys, "fetchPost", JS_NewCFunction(ctx, js_fetch_post, "fetchPost", 3)); JS_SetPropertyStr(ctx, sys, "fetchCancel", JS_NewCFunction(ctx, js_fetch_cancel, "fetchCancel", 0)); JS_SetPropertyStr(ctx, sys, "fetchPending", JS_NewBool(ctx, current_rt ? current_rt->fetch_pending : 0)); if (current_rt && current_rt->fetch_pending) { FILE *rc = fopen("/tmp/ac_fetch_rc", "r"); if (rc) { int code = -1; fscanf(rc, "%d", &code); fclose(rc); unlink("/tmp/ac_fetch_rc"); ac_log("[fetch] done: curl exit=%d\n", code); current_rt->fetch_result[0] = 0; current_rt->fetch_error[0] = 0; if (code == 0) { FILE *fp = fopen("/tmp/ac_fetch.json", "r"); if (fp) { int n = (int)fread(current_rt->fetch_result, 1, sizeof(current_rt->fetch_result) - 1, fp); fclose(fp); current_rt->fetch_result[n] = 0; unlink("/tmp/ac_fetch.json"); } else { snprintf(current_rt->fetch_error, sizeof(current_rt->fetch_error), "request failed: missing response body"); } unlink("/tmp/ac_fetch_err"); } else { char emsg[160] = {0}; FILE *ef = fopen("/tmp/ac_fetch_err", "r"); if (ef) { int en = (int)fread(emsg, 1, sizeof(emsg) - 1, ef); fclose(ef); emsg[en] = 0; for (int i = 0; emsg[i]; i++) { if (emsg[i] == '\n' || emsg[i] == '\r' || emsg[i] == '\t') emsg[i] = ' '; } } unlink("/tmp/ac_fetch_err"); ac_log("[fetch] error (%d): %s\n", code, emsg[0] ? emsg : "(no stderr)"); if (emsg[0]) { snprintf(current_rt->fetch_error, sizeof(current_rt->fetch_error), "request failed (%d): %s", code, emsg); } else { snprintf(current_rt->fetch_error, sizeof(current_rt->fetch_error), "request failed (%d)", code); } } current_rt->fetch_pending = 0; } } // Deliver fetch result only during paint phase (where JS consumes it) // and clear after delivery (one-shot) if (current_rt && current_rt->fetch_result[0] && strcmp(current_phase, "paint") == 0) { JS_SetPropertyStr(ctx, sys, "fetchResult", JS_NewString(ctx, current_rt->fetch_result)); current_rt->fetch_result[0] = 0; } else { JS_SetPropertyStr(ctx, sys, "fetchResult", JS_NULL); } // Fetch error is also one-shot, delivered during paint. if (current_rt && current_rt->fetch_error[0] && strcmp(current_phase, "paint") == 0) { JS_SetPropertyStr(ctx, sys, "fetchError", JS_NewString(ctx, current_rt->fetch_error)); current_rt->fetch_error[0] = 0; } else { JS_SetPropertyStr(ctx, sys, "fetchError", JS_NULL); }
// QR camera scanning — system.scanQR() / system.scanQRStop() JS_SetPropertyStr(ctx, sys, "scanQR", JS_NewCFunction(ctx, js_scan_qr, "scanQR", 0)); JS_SetPropertyStr(ctx, sys, "scanQRStop", JS_NewCFunction(ctx, js_scan_qr_stop, "scanQRStop", 0)); JS_SetPropertyStr(ctx, sys, "cameraBlit", JS_NewCFunction(ctx, js_camera_blit, "cameraBlit", 6));
// Continuous camera streaming + tape control — the 'cap' piece JS_SetPropertyStr(ctx, sys, "cameraStart", JS_NewCFunction(ctx, js_camera_start, "cameraStart", 0)); JS_SetPropertyStr(ctx, sys, "cameraStop", JS_NewCFunction(ctx, js_camera_stop, "cameraStop", 0)); JS_SetPropertyStr(ctx, sys, "cameraReady", JS_NewCFunction(ctx, js_camera_ready, "cameraReady", 0)); JS_SetPropertyStr(ctx, sys, "cameraError", JS_NewCFunction(ctx, js_camera_error, "cameraError", 0)); JS_SetPropertyStr(ctx, sys, "cameraSwitch", JS_NewCFunction(ctx, js_camera_switch, "cameraSwitch", 0)); JS_SetPropertyStr(ctx, sys, "cameraCount", JS_NewCFunction(ctx, js_camera_count, "cameraCount", 0)); JS_SetPropertyStr(ctx, sys, "tapeStart", JS_NewCFunction(ctx, js_tape_start, "tapeStart", 0)); JS_SetPropertyStr(ctx, sys, "tapeStop", JS_NewCFunction(ctx, js_tape_stop, "tapeStop", 0)); JS_SetPropertyStr(ctx, sys, "qrPending", JS_NewBool(ctx, current_rt ? current_rt->qr_scan_active : 0)); // Deliver QR result one-shot during sim phase if (current_rt && current_rt->camera.scan_done) { if (current_rt->camera.scan_result[0]) { JS_SetPropertyStr(ctx, sys, "qrResult", JS_NewString(ctx, current_rt->camera.scan_result)); current_rt->camera.scan_result[0] = 0; } else if (current_rt->camera.scan_error[0]) { JS_SetPropertyStr(ctx, sys, "qrError", JS_NewString(ctx, current_rt->camera.scan_error)); current_rt->camera.scan_error[0] = 0; } else { JS_SetPropertyStr(ctx, sys, "qrResult", JS_NULL); } current_rt->camera.scan_done = 0; } else { JS_SetPropertyStr(ctx, sys, "qrResult", JS_NULL); JS_SetPropertyStr(ctx, sys, "qrError", JS_NULL); }
// OS update version string — matches OTA format: "buildname githash-buildts". // The nine-way #ifdef ladder this replaces existed only because the parts // were macros that might not be defined; version.c always defines them, so // one composed string does the same job. JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, ac_version_long()));
// Firmware capability probe — exposed as `system.firmware` so os.mjs can // gate the firmware-update panel on machines where flashing is actually // viable. Criteria, all of which must be true: // 1. /dev/mtd0 exists — kernel has a usable SPI-NOR driver bound to // the motherboard's flash chip (CONFIG_SPI_INTEL_PCI + CONFIG_MTD). // Absent this, flashrom's internal programmer can't open the chip. // 2. bios_vendor contains "coreboot" — the only firmware we support // reflashing is MrChromebox's coreboot+edk2 build. Stock OEM AMI / // Insyde firmware would either reject writes (SMM) or brick. // 3. Optional: a plausible Chromebook/supported-board identifier in // product_name, which lets us suggest a MrChromebox ROM URL. // This is an advisory flag; the actual flash is gated by a second probe // + `flashrom --probe` step inside the update thread. { JSValue firmware = JS_NewObject(ctx); int mtd_ok = (access("/dev/mtd0", F_OK) == 0); char bios_vendor[128] = ""; char product_name[128] = ""; char bios_version[128] = ""; read_sysfs("/sys/class/dmi/id/bios_vendor", bios_vendor, sizeof(bios_vendor)); read_sysfs("/sys/class/dmi/id/product_name", product_name, sizeof(product_name)); read_sysfs("/sys/class/dmi/id/bios_version", bios_version, sizeof(bios_version)); // Trim trailing newlines from sysfs reads for (char *p = bios_vendor; *p; p++) if (*p == '\n') { *p = 0; break; } for (char *p = product_name; *p; p++) if (*p == '\n') { *p = 0; break; } for (char *p = bios_version; *p; p++) if (*p == '\n') { *p = 0; break; } int coreboot = (strstr(bios_vendor, "coreboot") != NULL) || (strstr(bios_version, "MrChromebox") != NULL); int available = mtd_ok && coreboot; JS_SetPropertyStr(ctx, firmware, "available", JS_NewBool(ctx, available)); JS_SetPropertyStr(ctx, firmware, "mtdOk", JS_NewBool(ctx, mtd_ok)); JS_SetPropertyStr(ctx, firmware, "coreboot", JS_NewBool(ctx, coreboot)); JS_SetPropertyStr(ctx, firmware, "biosVendor", JS_NewString(ctx, bios_vendor)); JS_SetPropertyStr(ctx, firmware, "biosVersion", JS_NewString(ctx, bios_version)); JS_SetPropertyStr(ctx, firmware, "productName", JS_NewString(ctx, product_name)); // Lowercase board name (first word of product_name) is the URL slug // MrChromebox uses: e.g. "Google Drawman/Drawcia" -> "drawcia" char board[64] = ""; { const char *src = product_name; // Skip "Google " prefix if present if (strncmp(src, "Google ", 7) == 0) src += 7; // Copy up to '/' or ' ' or end, lowercasing int i = 0; while (*src && *src != '/' && *src != ' ' && i < (int)sizeof(board) - 1) { board[i++] = (*src >= 'A' && *src <= 'Z') ? (*src + 32) : *src; src++; } board[i] = 0; } JS_SetPropertyStr(ctx, firmware, "board", JS_NewString(ctx, board)); // Runtime install state — live values updated by fw_thread_fn. if (current_rt) { JS_SetPropertyStr(ctx, firmware, "pending", JS_NewBool(ctx, current_rt->fw_pending)); JS_SetPropertyStr(ctx, firmware, "done", JS_NewBool(ctx, current_rt->fw_done)); JS_SetPropertyStr(ctx, firmware, "ok", JS_NewBool(ctx, current_rt->fw_ok)); if (current_rt->fw_backup_path[0]) JS_SetPropertyStr(ctx, firmware, "backupPath", JS_NewString(ctx, current_rt->fw_backup_path)); JSValue log = JS_NewArray(ctx); int count = current_rt->fw_log_count; int show = count < 32 ? count : 32; int start = count - show; for (int i = 0; i < show; i++) { JS_SetPropertyUint32(ctx, log, i, JS_NewString(ctx, current_rt->fw_log[(start + i) % 32])); } JS_SetPropertyStr(ctx, firmware, "log", log); } // Action: system.firmware.install(mode?) — "install" | "dry-run" | "restore" JS_SetPropertyStr(ctx, firmware, "install", JS_NewCFunction(ctx, js_firmware_install, "install", 1)); JS_SetPropertyStr(ctx, sys, "firmware", firmware); }
// Audio diagnostic API — speaker.mjs piece uses these to probe which // ALSA PCM actually produces sound on this machine. listPcms scans // /proc/asound; testPcm plays a short sine tone on the named device // in a detached thread. Both are read-only — no state change to the // main audio thread. { JSValue audio = JS_NewObject(ctx); JS_SetPropertyStr(ctx, audio, "listPcms", JS_NewCFunction(ctx, js_audio_list_pcms, "listPcms", 0)); JS_SetPropertyStr(ctx, audio, "testPcm", JS_NewCFunction(ctx, js_audio_test_pcm, "testPcm", 4)); /* Report which device ac-native's main audio thread is currently * using — helpful context when the user is probing alternatives. */ if (current_rt && current_rt->audio && current_rt->audio->audio_device[0]) { JS_SetPropertyStr(ctx, audio, "activeDevice", JS_NewString(ctx, current_rt->audio->audio_device)); } JS_SetPropertyStr(ctx, sys, "audio", audio); }
// User config (handle, piece — read from /mnt/config.json at boot) { JSValue config = JS_NewObject(ctx); if (current_rt && current_rt->handle[0]) JS_SetPropertyStr(ctx, config, "handle", JS_NewString(ctx, current_rt->handle)); if (current_rt && current_rt->piece[0]) JS_SetPropertyStr(ctx, config, "piece", JS_NewString(ctx, current_rt->piece)); // Expose raw config JSON so pieces can read arbitrary fields { static char cfg_cache[8192] = {0}; if (config_cache_dirty) { cfg_cache[0] = '\0'; FILE *cf = fopen("/mnt/config.json", "r"); if (cf) { size_t n = fread(cfg_cache, 1, sizeof(cfg_cache) - 1, cf); cfg_cache[n] = '\0'; fclose(cf); } config_cache_dirty = 0; } if (cfg_cache[0]) JS_SetPropertyStr(ctx, config, "raw", JS_NewString(ctx, cfg_cache)); } JS_SetPropertyStr(ctx, sys, "config", config); }
// Boot device detection (cached — detect once, not every frame) { static char boot_dev_cache[64] = {0}; if (!boot_dev_cache[0]) detect_boot_device(boot_dev_cache, sizeof(boot_dev_cache)); JS_SetPropertyStr(ctx, sys, "bootDevice", JS_NewString(ctx, boot_dev_cache)); }
// Flash targets: enumerate all devices that could receive an EFI flash { JSValue targets = JS_NewArray(ctx); int idx = 0; // Check USB (/dev/sda1) if (access("/dev/sda1", F_OK) == 0) { char rem[8] = {0}; read_sysfs("/sys/block/sda/removable", rem, sizeof(rem)); int is_removable = (rem[0] == '1'); JSValue t = JS_NewObject(ctx); JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, "/dev/sda1")); JS_SetPropertyStr(ctx, t, "label", JS_NewString(ctx, is_removable ? "USB" : "Disk (sda)")); JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, is_removable)); JS_SetPropertyUint32(ctx, targets, idx++, t); } // Check NVMe (/dev/nvme0n1p1) if (access("/dev/nvme0n1p1", F_OK) == 0) { JSValue t = JS_NewObject(ctx); JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, "/dev/nvme0n1p1")); JS_SetPropertyStr(ctx, t, "label", JS_NewString(ctx, "Internal (NVMe)")); JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, 0)); JS_SetPropertyUint32(ctx, targets, idx++, t); } // Check eMMC (/dev/mmcblk0p1) — Chromebooks + some budget laptops use // eMMC instead of NVMe. Parent device is mmcblk0, partitions are // mmcblk0p1 etc. — same "p<N>" suffix scheme as NVMe. if (access("/dev/mmcblk0p1", F_OK) == 0) { JSValue t = JS_NewObject(ctx); JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, "/dev/mmcblk0p1")); JS_SetPropertyStr(ctx, t, "label", JS_NewString(ctx, "Internal (eMMC)")); JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, 0)); JS_SetPropertyUint32(ctx, targets, idx++, t); } // Check sdb1 (second USB) if (access("/dev/sdb1", F_OK) == 0) { char rem[8] = {0}; read_sysfs("/sys/block/sdb/removable", rem, sizeof(rem)); JSValue t = JS_NewObject(ctx); JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, "/dev/sdb1")); JS_SetPropertyStr(ctx, t, "label", JS_NewString(ctx, rem[0] == '1' ? "USB (sdb)" : "Disk (sdb)")); JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, rem[0] == '1')); JS_SetPropertyUint32(ctx, targets, idx++, t); } // Blank whole-disks — disks that exist but have no usable // partition[1]. These need format-and-install (sfdisk → mkfs.vfat // → write boot tree). Marked `blank: true` so os.mjs can render // them differently and prompt before wiping. struct { const char *disk; const char *part1; const char *label; const char *bus; } whole_disks[] = { {"/dev/nvme0n1", "/dev/nvme0n1p1", "Internal (NVMe — blank)", "nvme0n1"}, {"/dev/mmcblk0", "/dev/mmcblk0p1", "Internal (eMMC — blank)", "mmcblk0"}, {"/dev/sda", "/dev/sda1", "Disk sda — blank", "sda"}, {"/dev/sdb", "/dev/sdb1", "Disk sdb — blank", "sdb"}, {NULL, NULL, NULL, NULL}, }; for (int i = 0; whole_disks[i].disk; i++) { // Disk node must exist AND its first partition must NOT exist. if (access(whole_disks[i].disk, F_OK) != 0) continue; if (access(whole_disks[i].part1, F_OK) == 0) continue; // already partitioned — covered above char rem[8] = {0}; char rempath[64]; snprintf(rempath, sizeof(rempath), "/sys/block/%s/removable", whole_disks[i].bus); read_sysfs(rempath, rem, sizeof(rem)); JSValue t = JS_NewObject(ctx); JS_SetPropertyStr(ctx, t, "device", JS_NewString(ctx, whole_disks[i].disk)); JS_SetPropertyStr(ctx, t, "label", JS_NewString(ctx, whole_disks[i].label)); JS_SetPropertyStr(ctx, t, "removable", JS_NewBool(ctx, rem[0] == '1')); JS_SetPropertyStr(ctx, t, "blank", JS_NewBool(ctx, 1)); JS_SetPropertyUint32(ctx, targets, idx++, t); } JS_SetPropertyStr(ctx, sys, "flashTargets", targets); }
// Binary fetch for OS update JS_SetPropertyStr(ctx, sys, "fetchBinary", JS_NewCFunction(ctx, js_fetch_binary, "fetchBinary", 3)); if (current_rt) { // Poll progress from file size if (current_rt->fetch_binary_pending && current_rt->fetch_binary_expected > 0 && current_rt->fetch_binary_dest[0]) { struct stat fst; if (stat(current_rt->fetch_binary_dest, &fst) == 0) { float p = (float)fst.st_size / (float)current_rt->fetch_binary_expected; if (p > 1.0f) p = 1.0f; current_rt->fetch_binary_progress = p; } } // Check if curl finished if (current_rt->fetch_binary_pending) { FILE *fbrc = fopen("/tmp/ac_fb_rc", "r"); if (fbrc) { int rc = -1; fscanf(fbrc, "%d", &rc); fclose(fbrc); unlink("/tmp/ac_fb_rc"); if (rc != 0) { char emsg[160] = {0}; FILE *ef = fopen("/tmp/ac_fb_err", "r"); if (ef) { int en = (int)fread(emsg, 1, sizeof(emsg) - 1, ef); fclose(ef); emsg[en] = 0; for (int i = 0; emsg[i]; i++) { if (emsg[i] == '\n' || emsg[i] == '\r' || emsg[i] == '\t') emsg[i] = ' '; } } ac_log("[fetchBinary] error (%d): %s\n", rc, emsg[0] ? emsg : "(no stderr)"); } unlink("/tmp/ac_fb_err"); current_rt->fetch_binary_pending = 0; current_rt->fetch_binary_done = 1; current_rt->fetch_binary_ok = (rc == 0) ? 1 : 0; current_rt->fetch_binary_progress = (rc == 0) ? 1.0f : 0.0f; ac_log("[fetchBinary] complete: rc=%d ok=%d dest=%s\n", rc, current_rt->fetch_binary_ok, current_rt->fetch_binary_dest); } } JS_SetPropertyStr(ctx, sys, "fetchBinaryProgress", JS_NewFloat64(ctx, (double)current_rt->fetch_binary_progress)); JS_SetPropertyStr(ctx, sys, "fetchBinaryDone", JS_NewBool(ctx, current_rt->fetch_binary_done)); JS_SetPropertyStr(ctx, sys, "fetchBinaryOk", JS_NewBool(ctx, current_rt->fetch_binary_ok)); // Keep done latched until next fetchBinary() call resets it. // Act/sim/paint run in separate calls; one-shot pulses can be lost. }
// Flash update JS_SetPropertyStr(ctx, sys, "flashUpdate", JS_NewCFunction(ctx, js_flash_update, "flashUpdate", 1)); JS_SetPropertyStr(ctx, sys, "reboot", JS_NewCFunction(ctx, js_reboot, "reboot", 0)); // Filesystem inspection — used by os.mjs OTA preflight + post-download // size verification so a hopeless install can be aborted before it // touches the ESP, and a truncated download is caught before flash. JS_SetPropertyStr(ctx, sys, "diskFreeBytes", JS_NewCFunction(ctx, js_disk_free_bytes, "diskFreeBytes", 1)); JS_SetPropertyStr(ctx, sys, "fileSizeBytes", JS_NewCFunction(ctx, js_file_size_bytes, "fileSizeBytes", 1)); if (current_rt) { JS_SetPropertyStr(ctx, sys, "flashDone", JS_NewBool(ctx, current_rt->flash_done)); JS_SetPropertyStr(ctx, sys, "flashOk", JS_NewBool(ctx, current_rt->flash_ok)); // Phase: 0=idle 1=writing 2=syncing 3=verifying 4=done JS_SetPropertyStr(ctx, sys, "flashPhase", JS_NewInt32(ctx, current_rt->flash_phase)); JS_SetPropertyStr(ctx, sys, "flashVerifiedBytes", JS_NewFloat64(ctx, (double)current_rt->flash_verified_bytes)); // Flash telemetry: destination path, same-device flag, log lines if (current_rt->flash_dst[0]) JS_SetPropertyStr(ctx, sys, "flashDst", JS_NewString(ctx, current_rt->flash_dst)); JS_SetPropertyStr(ctx, sys, "flashSameDevice", JS_NewBool(ctx, current_rt->flash_same_device)); // Flash log ring buffer { int count = current_rt->flash_log_count; int start = count > 16 ? count - 16 : 0; JSValue arr = JS_NewArray(ctx); for (int i = start; i < count; i++) { JS_SetPropertyUint32(ctx, arr, i - start, JS_NewString(ctx, current_rt->flash_log[i % 16])); } JS_SetPropertyStr(ctx, sys, "flashLog", arr); } }
// Config persistence JS_SetPropertyStr(ctx, sys, "saveConfig", JS_NewCFunction(ctx, js_save_config, "saveConfig", 2));
// Mono audio fold (also on sound.volume; here so prompt commands reach it) JS_SetPropertyStr(ctx, sys, "setMono", JS_NewCFunction(ctx, js_set_mono, "setMono", 1)); JS_SetPropertyStr(ctx, sys, "mono", JS_NewBool(ctx, current_rt && current_rt->audio ? current_rt->audio->mono : 0));
// SSH daemon JS_SetPropertyStr(ctx, sys, "startSSH", JS_NewCFunction(ctx, js_start_ssh, "startSSH", 0)); JS_SetPropertyStr(ctx, sys, "sshStarted", JS_NewBool(ctx, ssh_started));
// Node.js execution JS_SetPropertyStr(ctx, sys, "execNode", JS_NewCFunction(ctx, js_exec_node, "execNode", 1));
// PTY terminal emulator { JSValue pty_obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, pty_obj, "spawn", JS_NewCFunction(ctx, js_pty_spawn, "spawn", 4)); JS_SetPropertyStr(ctx, pty_obj, "write", JS_NewCFunction(ctx, js_pty_write, "write", 1)); JS_SetPropertyStr(ctx, pty_obj, "resize", JS_NewCFunction(ctx, js_pty_resize, "resize", 2)); JS_SetPropertyStr(ctx, pty_obj, "kill", JS_NewCFunction(ctx, js_pty_kill, "kill", 0)); JS_SetPropertyStr(ctx, pty_obj, "active", JS_NewBool(ctx, current_rt->pty_active));
// Pump PTY output and expose grid (only during paint phase) if (current_rt->pty_active) { pty_pump(¤t_rt->pty); pty_check_alive(¤t_rt->pty);
if (!current_rt->pty.alive) { // Drain remaining output (child error messages, etc.) before closing pty_pump(¤t_rt->pty); JS_SetPropertyStr(ctx, pty_obj, "exitCode", JS_NewInt32(ctx, current_rt->pty.exit_code)); pty_destroy(¤t_rt->pty); current_rt->pty_active = 0; }
JS_SetPropertyStr(ctx, pty_obj, "alive", JS_NewBool(ctx, current_rt->pty.alive)); JS_SetPropertyStr(ctx, pty_obj, "cursorX", JS_NewInt32(ctx, current_rt->pty.cursor_x)); JS_SetPropertyStr(ctx, pty_obj, "cursorY", JS_NewInt32(ctx, current_rt->pty.cursor_y)); JS_SetPropertyStr(ctx, pty_obj, "cols", JS_NewInt32(ctx, current_rt->pty.cols)); JS_SetPropertyStr(ctx, pty_obj, "rows", JS_NewInt32(ctx, current_rt->pty.rows)); JS_SetPropertyStr(ctx, pty_obj, "dirty", JS_NewBool(ctx, current_rt->pty.grid_dirty)); JS_SetPropertyStr(ctx, pty_obj, "cursorVisible", JS_NewBool(ctx, current_rt->pty.cursor_visible));
// Expose grid as flat array: [ch, fg, bg, bold, ...] per cell, row by row // Only send if dirty (perf optimization) if (current_rt->pty.grid_dirty) { ACPty *p = ¤t_rt->pty; JSValue grid = JS_NewArray(ctx); int idx = 0; for (int y = 0; y < p->rows; y++) { for (int x = 0; x < p->cols; x++) { ACPtyCell *c = &p->grid[y][x]; // Pack cell: ch, fg, bg, bold, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->ch)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bold)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_r)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_g)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_b)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_r)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_g)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_b)); } } JS_SetPropertyStr(ctx, pty_obj, "grid", grid); p->grid_dirty = 0; } } else { // PTY not active — expose last exit code for diagnostics JS_SetPropertyStr(ctx, pty_obj, "exitCode", JS_NewInt32(ctx, current_rt->pty.exit_code)); }
JS_SetPropertyStr(ctx, sys, "pty", pty_obj); }
// PTY2 — second terminal emulator for split-screen { JSValue pty2_obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, pty2_obj, "spawn", JS_NewCFunction(ctx, js_pty2_spawn, "spawn", 4)); JS_SetPropertyStr(ctx, pty2_obj, "write", JS_NewCFunction(ctx, js_pty2_write, "write", 1)); JS_SetPropertyStr(ctx, pty2_obj, "resize", JS_NewCFunction(ctx, js_pty2_resize, "resize", 2)); JS_SetPropertyStr(ctx, pty2_obj, "kill", JS_NewCFunction(ctx, js_pty2_kill, "kill", 0)); JS_SetPropertyStr(ctx, pty2_obj, "active", JS_NewBool(ctx, current_rt->pty2_active));
if (current_rt->pty2_active) { pty_pump(¤t_rt->pty2); pty_check_alive(¤t_rt->pty2);
if (!current_rt->pty2.alive) { pty_pump(¤t_rt->pty2); JS_SetPropertyStr(ctx, pty2_obj, "exitCode", JS_NewInt32(ctx, current_rt->pty2.exit_code)); pty_destroy(¤t_rt->pty2); current_rt->pty2_active = 0; }
JS_SetPropertyStr(ctx, pty2_obj, "alive", JS_NewBool(ctx, current_rt->pty2.alive)); JS_SetPropertyStr(ctx, pty2_obj, "cursorX", JS_NewInt32(ctx, current_rt->pty2.cursor_x)); JS_SetPropertyStr(ctx, pty2_obj, "cursorY", JS_NewInt32(ctx, current_rt->pty2.cursor_y)); JS_SetPropertyStr(ctx, pty2_obj, "cols", JS_NewInt32(ctx, current_rt->pty2.cols)); JS_SetPropertyStr(ctx, pty2_obj, "rows", JS_NewInt32(ctx, current_rt->pty2.rows)); JS_SetPropertyStr(ctx, pty2_obj, "dirty", JS_NewBool(ctx, current_rt->pty2.grid_dirty)); JS_SetPropertyStr(ctx, pty2_obj, "cursorVisible", JS_NewBool(ctx, current_rt->pty2.cursor_visible));
if (current_rt->pty2.grid_dirty) { ACPty *p = ¤t_rt->pty2; JSValue grid = JS_NewArray(ctx); int idx = 0; for (int y = 0; y < p->rows; y++) { for (int x = 0; x < p->cols; x++) { ACPtyCell *c = &p->grid[y][x]; JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->ch)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bold)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_r)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_g)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->fg_b)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_r)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_g)); JS_SetPropertyUint32(ctx, grid, idx++, JS_NewInt32(ctx, c->bg_b)); } } JS_SetPropertyStr(ctx, pty2_obj, "grid", grid); p->grid_dirty = 0; } } else { JS_SetPropertyStr(ctx, pty2_obj, "exitCode", JS_NewInt32(ctx, current_rt->pty2.exit_code)); }
JS_SetPropertyStr(ctx, sys, "pty2", pty2_obj); }
// Piece navigation JS_SetPropertyStr(ctx, sys, "jump", JS_NewCFunction(ctx, js_jump, "jump", 1));
// system.listPieces() — scan /pieces/*.mjs and return name array JS_SetPropertyStr(ctx, sys, "listPieces", JS_NewCFunction(ctx, js_list_pieces, "listPieces", 0));
// system.raylibTest(painting, frame?) — fill a painting buffer using // raylib's software (Image*) APIs. Returns false when raylib isn't // compiled in, so pieces can probe availability. JS_SetPropertyStr(ctx, sys, "raylibTest", JS_NewCFunction(ctx, js_raylib_test, "raylibTest", 2));
// Outside-in remote prompt round-trip (cmd:"prompt" via /machines). JS_SetPropertyStr(ctx, sys, "consumePromptCmd", JS_NewCFunction(ctx, js_consume_prompt_cmd, "consumePromptCmd", 0)); JS_SetPropertyStr(ctx, sys, "machinesResponse", JS_NewCFunction(ctx, js_machines_response, "machinesResponse", 3));
// Printer detection and raw printing JS_SetPropertyStr(ctx, sys, "listPrinters", JS_NewCFunction(ctx, js_list_printers, "listPrinters", 0)); JS_SetPropertyStr(ctx, sys, "printRaw", JS_NewCFunction(ctx, js_print_raw, "printRaw", 2)); JS_SetPropertyStr(ctx, sys, "dmxSend", JS_NewCFunction(ctx, js_dmx_send, "dmxSend", 1));
// Volume and brightness control from JS JS_SetPropertyStr(ctx, sys, "volumeAdjust", JS_NewCFunction(ctx, js_volume_adjust, "volumeAdjust", 1)); JS_SetPropertyStr(ctx, sys, "brightnessAdjust", JS_NewCFunction(ctx, js_brightness_adjust, "brightnessAdjust", 1)); JS_SetPropertyStr(ctx, sys, "qrEncode", JS_NewCFunction(ctx, js_qr_encode, "qrEncode", 1)); JS_SetPropertyStr(ctx, sys, "openBrowser", JS_NewCFunction(ctx, js_open_browser, "openBrowser", 1));
// Nopaint system — persistent painting canvas if (current_rt && strcmp(current_rt->system_mode, "nopaint") == 0) { // Create painting + buffer on first use if (!current_rt->nopaint_painting) { int w = current_rt->graph->screen->width; int h = current_rt->graph->screen->height; current_rt->nopaint_painting = fb_create(w, h); current_rt->nopaint_buffer = fb_create(w, h); // Fill painting with theme background fb_clear(current_rt->nopaint_painting, 0xFFF0EEE8); // light bg default fb_clear(current_rt->nopaint_buffer, 0x00000000); // transparent current_rt->nopaint_active = 1; ac_log("[nopaint] Created painting %dx%d\n", w, h); }
JSValue np = JS_NewObject(ctx);
// nopaint.is(state) — check nopaint state JS_SetPropertyStr(ctx, np, "is", JS_NewCFunction(ctx, js_nopaint_is, "is", 1)); JS_SetPropertyStr(ctx, np, "cancelStroke", JS_NewCFunction(ctx, js_nopaint_cancel, "cancelStroke", 0));
// nopaint.brush — current brush position JSValue brush = JS_NewObject(ctx); JS_SetPropertyStr(ctx, brush, "x", JS_NewInt32(ctx, current_rt->nopaint_brush_x)); JS_SetPropertyStr(ctx, brush, "y", JS_NewInt32(ctx, current_rt->nopaint_brush_y)); JS_SetPropertyStr(ctx, np, "brush", brush);
// nopaint.buffer — temporary stroke overlay (as Painting object) JSValue buf_obj = JS_NewObjectClass(ctx, painting_class_id); JS_SetOpaque(buf_obj, current_rt->nopaint_buffer); JS_SetPropertyStr(ctx, buf_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_buffer->width)); JS_SetPropertyStr(ctx, buf_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_buffer->height)); JS_SetPropertyStr(ctx, np, "buffer", buf_obj);
// nopaint.needsBake JS_SetPropertyStr(ctx, np, "needsBake", JS_NewBool(ctx, current_rt->nopaint_needs_bake));
JS_SetPropertyStr(ctx, sys, "nopaint", np);
// system.painting — the persistent canvas (as Painting object) JSValue ptg_obj = JS_NewObjectClass(ctx, painting_class_id); JS_SetOpaque(ptg_obj, current_rt->nopaint_painting); JS_SetPropertyStr(ctx, ptg_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_painting->width)); JS_SetPropertyStr(ctx, ptg_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_painting->height)); JS_SetPropertyStr(ctx, sys, "painting", ptg_obj); } else if (current_rt && current_rt->nopaint_painting) { // Even non-nopaint pieces (like prompt) can access system.painting to show it JSValue ptg_obj = JS_NewObjectClass(ctx, painting_class_id); JS_SetOpaque(ptg_obj, current_rt->nopaint_painting); JS_SetPropertyStr(ctx, ptg_obj, "width", JS_NewInt32(ctx, current_rt->nopaint_painting->width)); JS_SetPropertyStr(ctx, ptg_obj, "height", JS_NewInt32(ctx, current_rt->nopaint_painting->height)); JS_SetPropertyStr(ctx, sys, "painting", ptg_obj); }
return sys;}
// Build the API object passed to lifecycle functionsstatic JSValue build_api(JSContext *ctx, ACRuntime *rt, const char *phase) { current_phase = phase; JSValue api = JS_NewObject(ctx); JSValue global = JS_GetGlobalObject(ctx);
// Graphics JS_SetPropertyStr(ctx, api, "wipe", JS_GetPropertyStr(ctx, global, "wipe")); JS_SetPropertyStr(ctx, api, "ink", JS_GetPropertyStr(ctx, global, "ink")); JS_SetPropertyStr(ctx, api, "line", JS_GetPropertyStr(ctx, global, "line")); JS_SetPropertyStr(ctx, api, "box", JS_GetPropertyStr(ctx, global, "box")); JS_SetPropertyStr(ctx, api, "circle", JS_GetPropertyStr(ctx, global, "circle")); JS_SetPropertyStr(ctx, api, "plot", JS_GetPropertyStr(ctx, global, "plot")); JS_SetPropertyStr(ctx, api, "write", JS_GetPropertyStr(ctx, global, "write")); JS_SetPropertyStr(ctx, api, "scroll", JS_GetPropertyStr(ctx, global, "scroll")); JS_SetPropertyStr(ctx, api, "blur", JS_GetPropertyStr(ctx, global, "blur")); JS_SetPropertyStr(ctx, api, "zoom", JS_GetPropertyStr(ctx, global, "zoom")); JS_SetPropertyStr(ctx, api, "contrast", JS_GetPropertyStr(ctx, global, "contrast")); JS_SetPropertyStr(ctx, api, "spin", JS_GetPropertyStr(ctx, global, "spin")); JS_SetPropertyStr(ctx, api, "qr", JS_GetPropertyStr(ctx, global, "qr"));
// screen { JSValue s = JS_NewObject(ctx); JS_SetPropertyStr(ctx, s, "width", JS_NewInt32(ctx, rt->graph->fb->width)); JS_SetPropertyStr(ctx, s, "height", JS_NewInt32(ctx, rt->graph->fb->height)); JS_SetPropertyStr(ctx, api, "screen", s); }
// Counters if (strcmp(phase, "paint") == 0) { JS_SetPropertyStr(ctx, api, "paintCount", JS_NewInt32(ctx, rt->paint_count)); } if (strcmp(phase, "sim") == 0) { JS_SetPropertyStr(ctx, api, "simCount", JS_NewInt32(ctx, rt->sim_count)); }
// Params (colon-separated args from system.jump, e.g. "chat:clock" → ["clock"]) if (strcmp(phase, "boot") == 0 && rt->jump_param_count > 0) { JSValue params = JS_NewArray(ctx); for (int i = 0; i < rt->jump_param_count; i++) { JS_SetPropertyUint32(ctx, params, i, JS_NewString(ctx, rt->jump_params[i])); } JS_SetPropertyStr(ctx, api, "params", params); }
// needsPaint (noop — native always paints every frame) JS_SetPropertyStr(ctx, api, "needsPaint", JS_NewCFunction(ctx, js_noop, "needsPaint", 0));
// Sound JS_SetPropertyStr(ctx, api, "sound", build_sound_obj(ctx, rt));
// System (battery, etc) JS_SetPropertyStr(ctx, api, "system", build_system_obj(ctx));
// WiFi JS_SetPropertyStr(ctx, api, "wifi", build_wifi_obj(ctx, rt->wifi));
// Trackpad delta (per-frame relative movement) if (rt->input) { JSValue trackpad = JS_NewObject(ctx); JS_SetPropertyStr(ctx, trackpad, "dx", JS_NewInt32(ctx, rt->input->delta_x)); JS_SetPropertyStr(ctx, trackpad, "dy", JS_NewInt32(ctx, rt->input->delta_y));
// Whole-hand contacts. The tracker in input.c has kept real MT-B // slots since the pad became playable, but nothing carried them // across into JS, so a piece could only ever see the summed pointer. // A drum head needs the set: the finger that strikes AND the ones // resting, which damp the modes the strike excites. // // `generation` bumps only when a SYN_REPORT actually changed the // contact set, so a piece can skip work on frames where the hand // has not moved. `aspect` is the pad's true width/height from // EVIOCGABS — the membrane's boundary math needs the real shape, // not the square one normalized coordinates imply. ACTouchSlot pts[MAX_TOUCH_SLOTS]; int touch_n = input_touch_contacts(rt->input, pts, MAX_TOUCH_SLOTS); JSValue contacts = JS_NewArray(ctx); for (int i = 0; i < touch_n; i++) { JSValue c = JS_NewObject(ctx); JS_SetPropertyStr(ctx, c, "id", JS_NewInt32(ctx, pts[i].tracking_id)); JS_SetPropertyStr(ctx, c, "x", JS_NewFloat64(ctx, (double)pts[i].x)); JS_SetPropertyStr(ctx, c, "y", JS_NewFloat64(ctx, (double)pts[i].y)); JS_SetPropertyStr(ctx, c, "pressure", JS_NewFloat64(ctx, (double)pts[i].pressure)); JS_SetPropertyStr(ctx, c, "downAt", JS_NewFloat64(ctx, pts[i].down_at)); JS_SetPropertyUint32(ctx, contacts, (uint32_t)i, c); } JS_SetPropertyStr(ctx, trackpad, "contacts", contacts); JS_SetPropertyStr(ctx, trackpad, "aspect", JS_NewFloat64(ctx, (double)input_touch_aspect(rt->input))); JS_SetPropertyStr(ctx, trackpad, "generation", JS_NewInt32(ctx, rt->input->surface.generation)); JS_SetPropertyStr(ctx, trackpad, "grabbed", JS_NewBool(ctx, rt->surface_grab)); JS_SetPropertyStr(ctx, api, "trackpad", trackpad);
// Analog key pressures — object mapping key names to 0.0-1.0 pressure if (rt->input->has_analog) { JSValue pressures = JS_NewObject(ctx); for (int i = 0; i < MAX_ANALOG_KEYS; i++) { if (rt->input->analog_keys[i].active) { const char *name = input_key_name(rt->input->analog_keys[i].key_code); if (name) { JS_SetPropertyStr(ctx, pressures, name, JS_NewFloat64(ctx, (double)rt->input->analog_keys[i].pressure)); } } } JS_SetPropertyStr(ctx, api, "pressures", pressures); } }
// params, colon, query JS_SetPropertyStr(ctx, api, "params", JS_NewArray(ctx)); JS_SetPropertyStr(ctx, api, "colon", JS_NewArray(ctx)); JS_SetPropertyStr(ctx, api, "query", JS_NewObject(ctx));
// Form constructor JS_SetPropertyStr(ctx, api, "Form", JS_NewCFunction2(ctx, js_form_constructor, "Form", 2, JS_CFUNC_constructor, 0));
// CUBEL and QUAD geometry constants JS_SetPropertyStr(ctx, api, "CUBEL", build_cubel(ctx)); JS_SetPropertyStr(ctx, api, "QUAD", build_quad(ctx));
// penLock JS_SetPropertyStr(ctx, api, "penLock", JS_NewCFunction(ctx, js_pen_lock, "penLock", 0));
// get (stub for texture loading — fps.mjs checks if get is available) JS_SetPropertyStr(ctx, api, "get", JS_NULL);
// setShowClippedWireframes, clearWireframeBuffer, drawBufferedWireframes, // getRenderStats — stubs/no-ops for fps.mjs debug panel JS_SetPropertyStr(ctx, api, "setShowClippedWireframes", JS_NewCFunction(ctx, js_noop, "setShowClippedWireframes", 1)); JS_SetPropertyStr(ctx, api, "clearWireframeBuffer", JS_NewCFunction(ctx, js_noop, "clearWireframeBuffer", 0)); JS_SetPropertyStr(ctx, api, "drawBufferedWireframes", JS_NewCFunction(ctx, js_noop, "drawBufferedWireframes", 0)); JS_SetPropertyStr(ctx, api, "getRenderStats", JS_NewCFunction(ctx, js_noop, "getRenderStats", 0));
// system.fps — camera and render stats for debug panel { JSValue fps_obj = JS_NewObject(ctx);
// system.fps.renderStats JSValue rs = JS_NewObject(ctx); JS_SetPropertyStr(ctx, rs, "originalTriangles", JS_NewInt32(ctx, rt->render_stats.originalTriangles)); JS_SetPropertyStr(ctx, rs, "clippedTriangles", JS_NewInt32(ctx, rt->render_stats.clippedTriangles)); JS_SetPropertyStr(ctx, rs, "subdividedTriangles", JS_NewInt32(ctx, rt->render_stats.subdividedTriangles)); JS_SetPropertyStr(ctx, rs, "trianglesRejected", JS_NewInt32(ctx, rt->render_stats.trianglesRejected)); JS_SetPropertyStr(ctx, rs, "pixelsDrawn", JS_NewInt32(ctx, rt->render_stats.pixelsDrawn)); JS_SetPropertyStr(ctx, rs, "wireframeSegmentsTotal", JS_NewInt32(ctx, rt->render_stats.wireframeSegmentsTotal)); JS_SetPropertyStr(ctx, rs, "wireframeSegmentsTextured", JS_NewInt32(ctx, rt->render_stats.wireframeSegmentsTextured)); JS_SetPropertyStr(ctx, rs, "wireframeSegmentsGradient", JS_NewInt32(ctx, rt->render_stats.wireframeSegmentsGradient)); JS_SetPropertyStr(ctx, fps_obj, "renderStats", rs);
// system.fps.doll.cam — camera position/rotation for debug JSValue doll = JS_NewObject(ctx); JSValue cam = JS_NewObject(ctx); JS_SetPropertyStr(ctx, cam, "x", JS_NewFloat64(ctx, rt->camera3d.x)); JS_SetPropertyStr(ctx, cam, "y", JS_NewFloat64(ctx, rt->camera3d.y)); JS_SetPropertyStr(ctx, cam, "z", JS_NewFloat64(ctx, rt->camera3d.z)); JS_SetPropertyStr(ctx, cam, "rotX", JS_NewFloat64(ctx, rt->camera3d.rotX)); JS_SetPropertyStr(ctx, cam, "rotY", JS_NewFloat64(ctx, rt->camera3d.rotY)); JS_SetPropertyStr(ctx, cam, "rotZ", JS_NewFloat64(ctx, rt->camera3d.rotZ)); JS_SetPropertyStr(ctx, doll, "cam", cam); JS_SetPropertyStr(ctx, fps_obj, "doll", doll);
// Attach to system object on api JSValue sys = JS_GetPropertyStr(ctx, api, "system"); if (!JS_IsUndefined(sys) && !JS_IsNull(sys)) JS_SetPropertyStr(ctx, sys, "fps", fps_obj); else JS_FreeValue(ctx, fps_obj); JS_FreeValue(ctx, sys); }
// fps (no-op — kept for non-3D pieces) JS_SetPropertyStr(ctx, api, "fps", JS_NewCFunction(ctx, js_noop, "fps", 1));
// hud { JSValue hud = JS_NewObject(ctx); JS_SetPropertyStr(ctx, hud, "label", JS_NewCFunction(ctx, js_noop, "label", 3)); JS_SetPropertyStr(ctx, hud, "superscript", JS_NewCFunction(ctx, js_noop, "superscript", 1)); JS_SetPropertyStr(ctx, api, "hud", hud); }
// net { JSValue net = JS_NewObject(ctx); JS_SetPropertyStr(ctx, net, "udp", JS_NewCFunction(ctx, js_net_udp, "udp", 0)); JS_SetPropertyStr(ctx, net, "preload", JS_NewCFunction(ctx, js_promise_null, "preload", 1)); JS_SetPropertyStr(ctx, net, "pieces", JS_NewCFunction(ctx, js_promise_null, "pieces", 1)); JS_SetPropertyStr(ctx, net, "rewrite", JS_NewCFunction(ctx, js_noop, "rewrite", 1)); JS_SetPropertyStr(ctx, net, "log", JS_NewCFunction(ctx, js_noop, "log", 2)); JS_SetPropertyStr(ctx, api, "net", net); }
// pen — current pointer position if (rt->input) { JSValue pen = JS_NewObject(ctx); JS_SetPropertyStr(ctx, pen, "x", JS_NewInt32(ctx, rt->input->pointer_x)); JS_SetPropertyStr(ctx, pen, "y", JS_NewInt32(ctx, rt->input->pointer_y)); JS_SetPropertyStr(ctx, pen, "down", JS_NewBool(ctx, rt->input->pointer_down)); JS_SetPropertyStr(ctx, api, "pen", pen); }
// store { JSValue store = JS_NewObject(ctx); JS_SetPropertyStr(ctx, store, "retrieve", JS_NewCFunction(ctx, js_promise_null, "retrieve", 2)); JS_SetPropertyStr(ctx, store, "persist", JS_NewCFunction(ctx, js_noop, "persist", 2)); JS_SetPropertyStr(ctx, store, "delete", JS_NewCFunction(ctx, js_promise_null, "delete", 2)); JS_SetPropertyStr(ctx, api, "store", store); }
// clock { JSValue clock = JS_NewObject(ctx); JS_SetPropertyStr(ctx, clock, "resync", JS_NewCFunction(ctx, js_noop, "resync", 0)); JS_SetPropertyStr(ctx, clock, "time", JS_NewCFunction(ctx, js_clock_time, "time", 0)); JS_SetPropertyStr(ctx, api, "clock", clock); }
// gizmo.Hourglass (sim-tick timer) + seconds(n) → sim ticks. Native runs // sim once per ~60Hz frame, so seconds(n) = n * 60 (web is n * 120 with a // 120Hz sim loop — same wall-clock result either way). { JSValue gizmo = JS_NewObject(ctx); JS_SetPropertyStr(ctx, gizmo, "Hourglass", JS_GetPropertyStr(ctx, global, "__Hourglass")); JS_SetPropertyStr(ctx, api, "gizmo", gizmo); JS_SetPropertyStr(ctx, api, "seconds", JS_NewCFunction(ctx, js_seconds, "seconds", 1)); }
// speak(text, voice?, mode?, opts?) — top-level TTS (web parity; voice / // provider hints are ignored, the native espeak voice reads the text). JS_SetPropertyStr(ctx, api, "speak", JS_NewCFunction(ctx, js_speak, "speak", 4));
// typeface (no-op function) JS_SetPropertyStr(ctx, api, "typeface", JS_NewCFunction(ctx, js_noop, "typeface", 1));
// painting(w, h, callback) — creates a stub painting object with width/height JS_SetPropertyStr(ctx, api, "painting", JS_NewCFunction(ctx, js_painting, "painting", 3));
// paste, page (real implementations), layer, sharpen (stubs) JS_SetPropertyStr(ctx, api, "paste", JS_NewCFunction(ctx, js_paste, "paste", 3)); JS_SetPropertyStr(ctx, api, "page", JS_NewCFunction(ctx, js_page, "page", 1)); JS_SetPropertyStr(ctx, api, "layer", JS_NewCFunction(ctx, js_noop, "layer", 1)); JS_SetPropertyStr(ctx, api, "sharpen", JS_NewCFunction(ctx, js_noop, "sharpen", 1));
// num { JSValue num = JS_NewObject(ctx); JS_SetPropertyStr(ctx, num, "clamp", JS_NewCFunction(ctx, js_num_clamp, "clamp", 3)); JS_SetPropertyStr(ctx, num, "rand", JS_NewCFunction(ctx, js_num_rand, "rand", 0)); JS_SetPropertyStr(ctx, num, "randIntRange", JS_NewCFunction(ctx, js_num_randint, "randIntRange", 2)); JS_SetPropertyStr(ctx, num, "randInt", JS_NewCFunction(ctx, js_num_randint_single, "randInt", 1));
// max/min — just reference Math.max/min JSValue math = JS_GetPropertyStr(ctx, global, "Math"); JS_SetPropertyStr(ctx, num, "max", JS_GetPropertyStr(ctx, math, "max")); JS_SetPropertyStr(ctx, num, "min", JS_GetPropertyStr(ctx, math, "min")); JS_SetPropertyStr(ctx, num, "abs", JS_GetPropertyStr(ctx, math, "abs")); JS_SetPropertyStr(ctx, num, "floor", JS_GetPropertyStr(ctx, math, "floor")); JS_SetPropertyStr(ctx, num, "ceil", JS_GetPropertyStr(ctx, math, "ceil")); JS_SetPropertyStr(ctx, num, "round", JS_GetPropertyStr(ctx, math, "round")); JS_SetPropertyStr(ctx, num, "sign", JS_GetPropertyStr(ctx, math, "sign")); JS_FreeValue(ctx, math); JS_SetPropertyStr(ctx, num, "shiftRGB", JS_GetPropertyStr(ctx, global, "__shiftRGB")); JS_SetPropertyStr(ctx, num, "dist", JS_GetPropertyStr(ctx, global, "__dist")); JS_SetPropertyStr(ctx, num, "map", JS_GetPropertyStr(ctx, global, "__map")); JS_SetPropertyStr(ctx, num, "lerp", JS_GetPropertyStr(ctx, global, "__lerp")); JS_SetPropertyStr(ctx, num, "parseColor", JS_GetPropertyStr(ctx, global, "__parseColor")); JS_SetPropertyStr(ctx, num, "randIntArr", JS_GetPropertyStr(ctx, global, "__randIntArr")); JS_SetPropertyStr(ctx, num, "timestamp", JS_GetPropertyStr(ctx, global, "__timestamp")); JS_SetPropertyStr(ctx, api, "num", num); }
// help { JSValue help = JS_NewObject(ctx); JS_SetPropertyStr(ctx, help, "resampleArray", JS_NewCFunction(ctx, js_noop, "resampleArray", 2)); JS_SetPropertyStr(ctx, api, "help", help); }
// pens JS_SetPropertyStr(ctx, api, "pens", JS_NewCFunction(ctx, js_noop, "pens", 0));
// ui { JSValue ui = JS_NewObject(ctx); JSValue btn_ctor = JS_GetPropertyStr(ctx, global, "__Button"); JS_SetPropertyStr(ctx, ui, "Button", btn_ctor); JS_SetPropertyStr(ctx, api, "ui", ui); }
// api sub-object (meta-API) // notepat passes this nested `api` to helpers like buildWaveButton(api) // which destructure { screen, ui, typeface, geo, sound, num, ... } from it { JSValue api_meta = JS_NewObject(ctx); JS_SetPropertyStr(ctx, api_meta, "Typeface", JS_GetPropertyStr(ctx, global, "__Typeface")); JS_SetPropertyStr(ctx, api_meta, "beep", JS_NewCFunction(ctx, js_noop, "beep", 1)); JS_SetPropertyStr(ctx, api_meta, "send", JS_NewCFunction(ctx, js_noop, "send", 1));
// api.text.box JSValue api_text = JS_NewObject(ctx); JS_SetPropertyStr(ctx, api_text, "box", JS_NewCFunction(ctx, js_noop, "box", 5)); JS_SetPropertyStr(ctx, api_meta, "text", api_text);
// api.geo.Box JSValue api_geo = JS_NewObject(ctx); JS_SetPropertyStr(ctx, api_geo, "Box", JS_GetPropertyStr(ctx, global, "__Box")); JS_SetPropertyStr(ctx, api_meta, "geo", api_geo);
// Copy top-level API properties onto api_meta so helper functions // like buildWaveButton(api) can destructure { screen, ui, typeface, ... } JS_SetPropertyStr(ctx, api_meta, "screen", JS_GetPropertyStr(ctx, api, "screen")); JS_SetPropertyStr(ctx, api_meta, "ui", JS_GetPropertyStr(ctx, api, "ui")); JS_SetPropertyStr(ctx, api_meta, "typeface", JS_GetPropertyStr(ctx, api, "typeface")); JS_SetPropertyStr(ctx, api_meta, "sound", JS_GetPropertyStr(ctx, api, "sound")); JS_SetPropertyStr(ctx, api_meta, "num", JS_GetPropertyStr(ctx, api, "num")); JS_SetPropertyStr(ctx, api_meta, "hud", JS_GetPropertyStr(ctx, api, "hud")); JS_SetPropertyStr(ctx, api_meta, "net", JS_GetPropertyStr(ctx, api, "net")); JS_SetPropertyStr(ctx, api_meta, "store", JS_GetPropertyStr(ctx, api, "store")); JS_SetPropertyStr(ctx, api_meta, "help", JS_GetPropertyStr(ctx, api, "help")); JS_SetPropertyStr(ctx, api_meta, "painting", JS_GetPropertyStr(ctx, api, "painting")); JS_SetPropertyStr(ctx, api_meta, "paste", JS_GetPropertyStr(ctx, api, "paste")); JS_SetPropertyStr(ctx, api_meta, "page", JS_GetPropertyStr(ctx, api, "page")); JS_SetPropertyStr(ctx, api_meta, "layer", JS_GetPropertyStr(ctx, api, "layer")); JS_SetPropertyStr(ctx, api_meta, "pens", JS_GetPropertyStr(ctx, api, "pens")); JS_SetPropertyStr(ctx, api_meta, "params", JS_GetPropertyStr(ctx, api, "params")); JS_SetPropertyStr(ctx, api_meta, "colon", JS_GetPropertyStr(ctx, api, "colon")); JS_SetPropertyStr(ctx, api_meta, "wipe", JS_GetPropertyStr(ctx, api, "wipe")); JS_SetPropertyStr(ctx, api_meta, "ink", JS_GetPropertyStr(ctx, api, "ink")); JS_SetPropertyStr(ctx, api_meta, "box", JS_GetPropertyStr(ctx, api, "box")); JS_SetPropertyStr(ctx, api_meta, "line", JS_GetPropertyStr(ctx, api, "line")); JS_SetPropertyStr(ctx, api_meta, "circle", JS_GetPropertyStr(ctx, api, "circle")); JS_SetPropertyStr(ctx, api_meta, "plot", JS_GetPropertyStr(ctx, api, "plot")); JS_SetPropertyStr(ctx, api_meta, "write", JS_GetPropertyStr(ctx, api, "write")); JS_SetPropertyStr(ctx, api_meta, "scroll", JS_GetPropertyStr(ctx, api, "scroll")); JS_SetPropertyStr(ctx, api_meta, "blur", JS_GetPropertyStr(ctx, api, "blur")); JS_SetPropertyStr(ctx, api_meta, "zoom", JS_GetPropertyStr(ctx, api, "zoom")); JS_SetPropertyStr(ctx, api_meta, "contrast", JS_GetPropertyStr(ctx, api, "contrast")); JS_SetPropertyStr(ctx, api_meta, "spin", JS_GetPropertyStr(ctx, api, "spin"));
JS_SetPropertyStr(ctx, api, "api", api_meta); }
JS_FreeValue(ctx, global); return api;}
static int piece_load_counter = 0;
int js_load_piece(ACRuntime *rt, const char *path) { JSContext *ctx = rt->ctx; current_rt = rt;
FILE *f = fopen(path, "r"); if (!f) { fprintf(stderr, "[js] Cannot open %s\n", path); return -1; } fseek(f, 0, SEEK_END); long len = ftell(f); fseek(f, 0, SEEK_SET); char *src = malloc(len + 1); fread(src, 1, len, f); src[len] = '\0'; fclose(f);
// Append globalThis export assignments so we can find lifecycle functions // after module evaluation (QuickJS modules don't expose exports publicly). // For pieces with `export const system = "fps"` (arena.mjs etc.) this // ALSO instantiates CamDoll from the preloaded FPS bundle and wraps // boot/sim/act/paint so `api.system.fps.doll` is live on every call // without the piece needing to know anything about the native runtime. const char *export_shim = "\n;if(typeof boot==='function')globalThis.boot=boot;" "if(typeof paint==='function')globalThis.paint=paint;" "if(typeof act==='function')globalThis.act=act;" "if(typeof sim==='function')globalThis.sim=sim;" "if(typeof leave==='function')globalThis.leave=leave;" "if(typeof beat==='function')globalThis.beat=beat;" "if(typeof configureAutopat==='function')globalThis.configureAutopat=configureAutopat;" "if(typeof system!=='undefined')globalThis.__pieceSystem=system;" "if(typeof surface!=='undefined')globalThis.__pieceSurface=surface;" "if(typeof fpsOpts!=='undefined')globalThis.__pieceFpsOpts=fpsOpts;" // FPS system wiring — runs only when piece opts in. "if(globalThis.__pieceSystem==='fps'&&globalThis.__FpsSystem){" "try{" "const FS=globalThis.__FpsSystem;" "const opts=globalThis.__pieceFpsOpts||{fov:80};" "const doll=new FS.CamDoll(FS.Camera,FS.Dolly,opts);" "globalThis.__fpsDoll=doll;" "const _b=globalThis.boot,_s=globalThis.sim,_a=globalThis.act,_p=globalThis.paint;" "const inject=(api)=>{if(api){if(!api.system)api.system={};api.system.fps={doll};}};" "globalThis.boot=(api)=>{inject(api);return _b?.(api);};" "globalThis.sim=(api)=>{inject(api);try{doll.sim?.();}catch(e){}return _s?.(api);};" "globalThis.act=(api)=>{inject(api);try{if(api?.event)doll.act?.(api.event);}catch(e){}return _a?.(api);};" "globalThis.paint=(api)=>{inject(api);return _p?.(api);};" "}catch(e){console.error('[fps] wire-up failed:',e.message);}" "}\n"; size_t shim_len = strlen(export_shim); char *patched = malloc(len + shim_len + 1); memcpy(patched, src, len); memcpy(patched + len, export_shim, shim_len); patched[len + shim_len] = '\0'; free(src); src = patched; len += shim_len;
// Use unique module name each load (QuickJS caches modules by name) char mod_name[300]; snprintf(mod_name, sizeof(mod_name), "%s#%d", path, piece_load_counter++);
// Evaluate as module JSValue val = JS_Eval(ctx, src, len, mod_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); free(src);
if (JS_IsException(val)) { JSValue exc = JS_GetException(ctx); const char *str = JS_ToCString(ctx, exc); fprintf(stderr, "[js] Parse error: %s\n", str); JS_FreeCString(ctx, str); JS_FreeValue(ctx, exc); return -1; }
JSValue result = JS_EvalFunction(ctx, val); if (JS_IsException(result)) { JSValue exc = JS_GetException(ctx); const char *str = JS_ToCString(ctx, exc); fprintf(stderr, "[js] Module error: %s\n", str); JS_FreeCString(ctx, str); JS_FreeValue(ctx, exc); return -1; } JS_FreeValue(ctx, result);
// Get lifecycle functions from globalThis JSValue global = JS_GetGlobalObject(ctx); rt->boot_fn = JS_GetPropertyStr(ctx, global, "boot"); rt->paint_fn = JS_GetPropertyStr(ctx, global, "paint"); rt->act_fn = JS_GetPropertyStr(ctx, global, "act"); rt->sim_fn = JS_GetPropertyStr(ctx, global, "sim"); rt->leave_fn = JS_GetPropertyStr(ctx, global, "leave"); rt->beat_fn = JS_GetPropertyStr(ctx, global, "beat");
// Capture piece's `export const system` value (e.g. "prompt", "fps", "nopaint") rt->system_mode[0] = '\0'; JSValue sys_val = JS_GetPropertyStr(ctx, global, "__pieceSystem"); if (JS_IsString(sys_val)) { const char *s = JS_ToCString(ctx, sys_val); if (s) { strncpy(rt->system_mode, s, sizeof(rt->system_mode) - 1); rt->system_mode[sizeof(rt->system_mode) - 1] = '\0'; JS_FreeCString(ctx, s); } } JS_FreeValue(ctx, sys_val);
// Capture piece's `export const surface` value ("drum" = play the pad). rt->surface_mode[0] = '\0'; JSValue surf_val = JS_GetPropertyStr(ctx, global, "__pieceSurface"); if (JS_IsString(surf_val)) { const char *s = JS_ToCString(ctx, surf_val); if (s) { strncpy(rt->surface_mode, s, sizeof(rt->surface_mode) - 1); rt->surface_mode[sizeof(rt->surface_mode) - 1] = '\0'; JS_FreeCString(ctx, s); } } JS_FreeValue(ctx, surf_val);
// A piece that claims the surface takes the pointer with it. Load time is // the right moment to grab: the piece cannot miss the first strike waiting // for its own boot to ask. Swapping to a piece without the export releases // it, so a crashed or exited instrument can never strand a dead cursor. rt->surface_grab = (strcmp(rt->surface_mode, "drum") == 0) ? 1 : 0;
// Auto-detect FPS mode if (strcmp(rt->system_mode, "fps") == 0) rt->fps_system_active = 1; else rt->fps_system_active = 0;
JS_FreeValue(ctx, global);
fprintf(stderr, "[js] Loaded piece: %s (system=%s)\n", path, rt->system_mode[0] ? rt->system_mode : "(none)"); fprintf(stderr, "[js] boot=%s paint=%s act=%s sim=%s beat=%s\n", JS_IsFunction(ctx, rt->boot_fn) ? "yes" : "no", JS_IsFunction(ctx, rt->paint_fn) ? "yes" : "no", JS_IsFunction(ctx, rt->act_fn) ? "yes" : "no", JS_IsFunction(ctx, rt->sim_fn) ? "yes" : "no", JS_IsFunction(ctx, rt->beat_fn) ? "yes" : "no");
return 0;}
void js_call_boot(ACRuntime *rt) { if (!JS_IsFunction(rt->ctx, rt->boot_fn)) return; current_rt = rt; JSValue api = build_api(rt->ctx, rt, "boot"); JSValue result = JS_Call(rt->ctx, rt->boot_fn, JS_UNDEFINED, 1, &api); if (JS_IsException(result)) { JSValue exc = JS_GetException(rt->ctx); const char *str = JS_ToCString(rt->ctx, exc); JSValue stack = JS_GetPropertyStr(rt->ctx, exc, "stack"); const char *stack_str = JS_ToCString(rt->ctx, stack); fprintf(stderr, "[js] boot() error: %s\n%s\n", str, stack_str ? stack_str : ""); if (stack_str) JS_FreeCString(rt->ctx, stack_str); JS_FreeValue(rt->ctx, stack); JS_FreeCString(rt->ctx, str); JS_FreeValue(rt->ctx, exc); }
// If boot() returned a Promise (async function), we need to catch rejections if (JS_IsObject(result)) { // Check if it has a .catch method (Promise) JSValue catch_fn = JS_GetPropertyStr(rt->ctx, result, "catch"); if (JS_IsFunction(rt->ctx, catch_fn)) { // Register an error handler: promise.catch(e => console.error("boot async error:", e)) const char *catch_code = "(function(p) { p.catch(function(e) { console.error('[js] boot() async error:', e, e.stack || ''); }); })"; JSValue catch_handler = JS_Eval(rt->ctx, catch_code, strlen(catch_code), "<catch>", JS_EVAL_TYPE_GLOBAL); if (JS_IsFunction(rt->ctx, catch_handler)) { JSValue catch_result = JS_Call(rt->ctx, catch_handler, JS_UNDEFINED, 1, &result); JS_FreeValue(rt->ctx, catch_result); } JS_FreeValue(rt->ctx, catch_handler); } JS_FreeValue(rt->ctx, catch_fn); }
JS_FreeValue(rt->ctx, result); JS_FreeValue(rt->ctx, api);
// Execute pending jobs (promises) — boot() is async, drain fully // Each await creates a new microtask; need multiple drain rounds JSContext *pctx; for (int round = 0; round < 100; round++) { int jobs = JS_ExecutePendingJob(rt->rt, &pctx); if (jobs <= 0) break; }}
// Helper: record a JS crash for the overlaystatic void js_record_crash(ACRuntime *rt, const char *fn_name, const char *msg) { rt->crash_active = 1; rt->crash_count++; rt->crash_frame = 0; snprintf(rt->crash_msg, sizeof(rt->crash_msg), "%s(): %s", fn_name, msg ? msg : "unknown");}
void js_call_paint(ACRuntime *rt) { if (!JS_IsFunction(rt->ctx, rt->paint_fn)) return; current_rt = rt; rt->paint_count++;
// Nopaint: paste the persistent painting as background before piece paints if (rt->nopaint_active && rt->nopaint_painting) { graph_paste(rt->graph, rt->nopaint_painting, 0, 0); }
JSValue api = build_api(rt->ctx, rt, "paint"); JSValue result = JS_Call(rt->ctx, rt->paint_fn, JS_UNDEFINED, 1, &api); if (JS_IsException(result)) { JSValue exc = JS_GetException(rt->ctx); const char *str = JS_ToCString(rt->ctx, exc); JSValue stack = JS_GetPropertyStr(rt->ctx, exc, "stack"); const char *stack_str = JS_ToCString(rt->ctx, stack); fprintf(stderr, "[js] paint() error: %s\n%s\n", str, stack_str ? stack_str : ""); js_record_crash(rt, "paint", str); if (stack_str) JS_FreeCString(rt->ctx, stack_str); JS_FreeValue(rt->ctx, stack); JS_FreeCString(rt->ctx, str); JS_FreeValue(rt->ctx, exc); } JS_FreeValue(rt->ctx, result); JS_FreeValue(rt->ctx, api);
// Nopaint: if bake was requested, composite buffer → painting and clear buffer if (rt->nopaint_active && rt->nopaint_needs_bake) { if (rt->nopaint_buffer && rt->nopaint_painting) { graph_paste(rt->graph, rt->nopaint_buffer, 0, 0); // show final stroke on screen // Bake: composite buffer onto persistent painting ACFramebuffer *saved = rt->graph->fb; graph_page(rt->graph, rt->nopaint_painting); graph_paste(rt->graph, rt->nopaint_buffer, 0, 0); graph_page(rt->graph, saved); // Clear buffer fb_clear(rt->nopaint_buffer, 0x00000000); } rt->nopaint_needs_bake = 0; }}
void js_call_act(ACRuntime *rt) { current_rt = rt;
// Track key held state for FPS camera (process even without act_fn) ACInput *input = rt->input; for (int i = 0; i < input->event_count; i++) { ACEvent *ev = &input->events[i]; if (ev->type == AC_EVENT_KEYBOARD_DOWN && ev->key_code > 0 && ev->key_code < KEY_MAX) rt->keys_held[ev->key_code] = 1; else if (ev->type == AC_EVENT_KEYBOARD_UP && ev->key_code > 0 && ev->key_code < KEY_MAX) rt->keys_held[ev->key_code] = 0; }
// System-level Escape → jump to prompt (mirrors web bios behavior). // Skip if current piece IS prompt, or if piece uses system="world" or "prompt". if (strcmp(rt->system_mode, "prompt") != 0 && strcmp(rt->system_mode, "world") != 0) { for (int i = 0; i < input->event_count; i++) { ACEvent *ev = &input->events[i]; if (ev->type == AC_EVENT_KEYBOARD_DOWN && ev->key_code == KEY_ESC) { // A piece holding the surface owns its own exit. One Escape is // too cheap for an instrument — the key sits under the // drummer's left hand — so those pieces count their own // presses and jump when they mean it. Returning here would // steal the key before act() ever ran: that is why notepat's // triple-escape, warning tones and all, had never once fired // on this OS. Let the event through instead. if (rt->surface_grab) break; // Reset FPS camera state if active if (rt->pen_locked) { rt->pen_locked = 0; rt->fps_system_active = 0; } strncpy(rt->jump_target, "prompt", sizeof(rt->jump_target) - 1); rt->jump_target[sizeof(rt->jump_target) - 1] = '\0'; rt->jump_requested = 1; rt->jump_param_count = 0; ac_log("[system] Escape → prompt\n"); return; // Skip passing events to piece } } }
// A grabbed surface swallows pointer emulation. The contacts still reach // the piece through `api.trackpad.contacts`; what stops here is the single // synthesized pointer, which would otherwise have every drum strike also // pressing whatever on-screen control sat under the cursor. if (rt->surface_grab) { int kept = 0; for (int i = 0; i < input->event_count; i++) { ACEvent *ev = &input->events[i]; if (ev->type == AC_EVENT_TOUCH || ev->type == AC_EVENT_DRAW || ev->type == AC_EVENT_LIFT) continue; if (kept != i) input->events[kept] = *ev; kept++; } input->event_count = kept; }
// Nopaint: update brush position and state from touch/mouse events if (rt->nopaint_active && strcmp(rt->system_mode, "nopaint") == 0) { for (int i = 0; i < input->event_count; i++) { ACEvent *ev = &input->events[i]; if (ev->type == AC_EVENT_TOUCH) { rt->nopaint_state = 1; // painting rt->nopaint_brush_x = ev->x; rt->nopaint_brush_y = ev->y; rt->nopaint_needs_bake = 0; } else if (ev->type == AC_EVENT_DRAW) { if (rt->nopaint_state == 1) { rt->nopaint_brush_x = ev->x; rt->nopaint_brush_y = ev->y; } } else if (ev->type == AC_EVENT_LIFT) { if (rt->nopaint_state == 1) { rt->nopaint_state = 0; // idle rt->nopaint_needs_bake = 1; } } } }
if (!JS_IsFunction(rt->ctx, rt->act_fn)) return;
// One api per FRAME, not per event. build_api assembles the entire surface // a piece sees — on the order of 160 fresh function objects and 500 // property writes — and rebuilding it inside this loop multiplied all of // that by the event count, up to MAX_EVENTS_PER_FRAME. A held key repeating // or a hand moving across the trackpad was paying for dozens of complete // API graphs in a single frame, all of it immediately garbage. Only `event` // differs between iterations, so only `event` is replaced; the assignment // releases the previous one. if (input->event_count > 0) { JSValue api = build_api(rt->ctx, rt, "act"); for (int i = 0; i < input->event_count; i++) { JSValue event = make_event_object(rt->ctx, &input->events[i]); JS_SetPropertyStr(rt->ctx, api, "event", event);
JSValue result = JS_Call(rt->ctx, rt->act_fn, JS_UNDEFINED, 1, &api); if (JS_IsException(result)) { JSValue exc = JS_GetException(rt->ctx); const char *str = JS_ToCString(rt->ctx, exc); JSValue stack = JS_GetPropertyStr(rt->ctx, exc, "stack"); const char *stack_str = JS_ToCString(rt->ctx, stack); fprintf(stderr, "[js] act() error: %s\n%s\n", str, stack_str ? stack_str : ""); ac_log("[js] act() error: %s\n%s\n", str, stack_str ? stack_str : ""); js_record_crash(rt, "act", str); if (stack_str) JS_FreeCString(rt->ctx, stack_str); JS_FreeValue(rt->ctx, stack); JS_FreeCString(rt->ctx, str); JS_FreeValue(rt->ctx, exc); } JS_FreeValue(rt->ctx, result); } JS_FreeValue(rt->ctx, api); }}
void js_call_sim(ACRuntime *rt) { current_rt = rt;
// If the piece's FPS bundle installed a CamDoll (via the export // shim in js_load_piece), the doll's own sim() — invoked from the // wrapped piece sim() — is the camera authority. We just sync its // cam state back to rt->camera3d AFTER the piece sim runs (below). // Otherwise, fall back to the native WASD + trackpad driver. int have_doll = 0; { JSValue global = JS_GetGlobalObject(rt->ctx); JSValue doll = JS_GetPropertyStr(rt->ctx, global, "__fpsDoll"); have_doll = !JS_IsUndefined(doll) && !JS_IsNull(doll); JS_FreeValue(rt->ctx, doll); JS_FreeValue(rt->ctx, global); } if (!have_doll && rt->pen_locked && rt->fps_system_active) { camera3d_update(&rt->camera3d, rt->keys_held[KEY_W], rt->keys_held[KEY_S], rt->keys_held[KEY_A], rt->keys_held[KEY_D], rt->keys_held[KEY_SPACE], rt->keys_held[KEY_LEFTSHIFT], (float)(rt->input ? rt->input->delta_x : 0), (float)(rt->input ? rt->input->delta_y : 0)); // Consume trackpad delta if (rt->input) { rt->input->delta_x = 0; rt->input->delta_y = 0; } }
// Pump the global setTimeout queue (prelude __flushTimeouts) once per // sim tick — fires any callbacks whose deadline has passed. { JSValue global = JS_GetGlobalObject(rt->ctx); JSValue flush = JS_GetPropertyStr(rt->ctx, global, "__flushTimeouts"); if (JS_IsFunction(rt->ctx, flush)) { JSValue r = JS_Call(rt->ctx, flush, JS_UNDEFINED, 0, NULL); if (JS_IsException(r)) { JSValue exc = JS_GetException(rt->ctx); const char *str = JS_ToCString(rt->ctx, exc); fprintf(stderr, "[js] setTimeout cb error: %s\n", str ? str : "?"); if (str) JS_FreeCString(rt->ctx, str); JS_FreeValue(rt->ctx, exc); } JS_FreeValue(rt->ctx, r); } JS_FreeValue(rt->ctx, flush); JS_FreeValue(rt->ctx, global); }
if (!JS_IsFunction(rt->ctx, rt->sim_fn)) return; rt->sim_count++; JSValue api = build_api(rt->ctx, rt, "sim"); JSValue result = JS_Call(rt->ctx, rt->sim_fn, JS_UNDEFINED, 1, &api); if (JS_IsException(result)) { JSValue exc = JS_GetException(rt->ctx); const char *str = JS_ToCString(rt->ctx, exc); JSValue stack = JS_GetPropertyStr(rt->ctx, exc, "stack"); const char *stack_str = JS_ToCString(rt->ctx, stack); fprintf(stderr, "[js] sim() error: %s\n%s\n", str, stack_str ? stack_str : ""); js_record_crash(rt, "sim", str); if (stack_str) JS_FreeCString(rt->ctx, stack_str); JS_FreeValue(rt->ctx, stack); JS_FreeCString(rt->ctx, str); JS_FreeValue(rt->ctx, exc); } JS_FreeValue(rt->ctx, result); JS_FreeValue(rt->ctx, api);
// FPS camera sync — after the wrapped piece sim() runs (which // invoked doll.sim() via the shim), copy doll.cam.{x,y,z,rotX/Y/Z} // back into rt->camera3d so the next frame's graph3d rendering // uses the doll's camera. Only syncs when the doll is present. if (have_doll) { JSValue global = JS_GetGlobalObject(rt->ctx); JSValue doll = JS_GetPropertyStr(rt->ctx, global, "__fpsDoll"); if (!JS_IsUndefined(doll) && !JS_IsNull(doll)) { JSValue cam = JS_GetPropertyStr(rt->ctx, doll, "cam"); if (!JS_IsUndefined(cam) && !JS_IsNull(cam)) { double x, y, z, rx, ry, rz; JSValue v; v = JS_GetPropertyStr(rt->ctx, cam, "x"); if (JS_ToFloat64(rt->ctx, &x, v) == 0) rt->camera3d.x = (float)x; JS_FreeValue(rt->ctx, v); v = JS_GetPropertyStr(rt->ctx, cam, "y"); if (JS_ToFloat64(rt->ctx, &y, v) == 0) rt->camera3d.y = (float)y; JS_FreeValue(rt->ctx, v); v = JS_GetPropertyStr(rt->ctx, cam, "z"); if (JS_ToFloat64(rt->ctx, &z, v) == 0) rt->camera3d.z = (float)z; JS_FreeValue(rt->ctx, v); v = JS_GetPropertyStr(rt->ctx, cam, "rotX"); if (JS_ToFloat64(rt->ctx, &rx, v) == 0) rt->camera3d.rotX = (float)rx; JS_FreeValue(rt->ctx, v); v = JS_GetPropertyStr(rt->ctx, cam, "rotY"); if (JS_ToFloat64(rt->ctx, &ry, v) == 0) rt->camera3d.rotY = (float)ry; JS_FreeValue(rt->ctx, v); v = JS_GetPropertyStr(rt->ctx, cam, "rotZ"); if (JS_ToFloat64(rt->ctx, &rz, v) == 0) rt->camera3d.rotZ = (float)rz; JS_FreeValue(rt->ctx, v); } JS_FreeValue(rt->ctx, cam); } JS_FreeValue(rt->ctx, doll); JS_FreeValue(rt->ctx, global); }
// Execute pending jobs (promises) JSContext *pctx; while (JS_ExecutePendingJob(rt->rt, &pctx) > 0) {}}
void js_call_beat(ACRuntime *rt) { if (!JS_IsFunction(rt->ctx, rt->beat_fn)) return; current_rt = rt; JSValue api = build_api(rt->ctx, rt, "beat"); JSValue result = JS_Call(rt->ctx, rt->beat_fn, JS_UNDEFINED, 1, &api); if (JS_IsException(result)) { JSValue exc = JS_GetException(rt->ctx); const char *str = JS_ToCString(rt->ctx, exc); fprintf(stderr, "[js] beat() error: %s\n", str); JS_FreeCString(rt->ctx, str); JS_FreeValue(rt->ctx, exc); } JS_FreeValue(rt->ctx, result); JS_FreeValue(rt->ctx, api);}
void js_call_leave(ACRuntime *rt) { if (!JS_IsFunction(rt->ctx, rt->leave_fn)) return; current_rt = rt; JSValue result = JS_Call(rt->ctx, rt->leave_fn, JS_UNDEFINED, 0, NULL); JS_FreeValue(rt->ctx, result);}
void js_destroy(ACRuntime *rt) { if (!rt) return; JS_FreeValue(rt->ctx, rt->boot_fn); JS_FreeValue(rt->ctx, rt->paint_fn); JS_FreeValue(rt->ctx, rt->act_fn); JS_FreeValue(rt->ctx, rt->sim_fn); JS_FreeValue(rt->ctx, rt->leave_fn); JS_FreeValue(rt->ctx, rt->beat_fn); ws_destroy(rt->ws); udp_destroy(rt->udp); depth_destroy(rt->depth_buf); JS_FreeContext(rt->ctx); JS_FreeRuntime(rt->rt); free(rt);}