Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623#define _GNU_SOURCE#include "js-bindings.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 "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.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);}
// --- 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, "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, 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;}
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: first "model name" line from /proc/cpuinfo { FILE *cpuinfo = fopen("/proc/cpuinfo", "r"); char cpuline[256] = {0}; if (cpuinfo) { char line[512]; while (fgets(line, sizeof(line), cpuinfo)) { if (strncmp(line, "model name", 10) == 0) { char *colon = strchr(line, ':'); if (colon) { colon++; while (*colon == ' ') colon++; // Trim newline char *nl = strchr(colon, '\n'); if (nl) *nl = 0; strncpy(cpuline, colon, sizeof(cpuline) - 1); } break; } } fclose(cpuinfo); } JS_SetPropertyStr(ctx, hw, "cpu", JS_NewString(ctx, cpuline[0] ? cpuline : "unknown")); }
// CPU cores: count "processor" lines in /proc/cpuinfo { FILE *cpuinfo = fopen("/proc/cpuinfo", "r"); int cores = 0; if (cpuinfo) { char line[256]; while (fgets(line, sizeof(line), cpuinfo)) { if (strncmp(line, "processor", 9) == 0) cores++; } fclose(cpuinfo); } 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 name (baked in at compile time)#ifdef AC_BUILD_NAME JS_SetPropertyStr(ctx, hw, "buildName", JS_NewString(ctx, AC_BUILD_NAME));#endif#ifdef AC_GIT_HASH JS_SetPropertyStr(ctx, hw, "gitHash", JS_NewString(ctx, AC_GIT_HASH));#endif#ifdef AC_BUILD_TS JS_SetPropertyStr(ctx, hw, "buildTs", JS_NewString(ctx, AC_BUILD_TS));#endif
// 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")); } // Read GPU renderer from sysfs (Mesa exposes via DRI) char gpu_name[128] = "unknown"; FILE *fp = popen("cat /sys/kernel/debug/dri/0/name 2>/dev/null || " "cat /sys/class/drm/card0/device/label 2>/dev/null || " "echo unknown", "r"); if (fp) { if (fgets(gpu_name, sizeof(gpu_name), fp)) { // Strip trailing newline char *nl = strchr(gpu_name, '\n'); if (nl) *nl = '\0'; } pclose(fp); } 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"#ifdef AC_BUILD_NAME# ifdef AC_GIT_HASH# ifdef AC_BUILD_TS JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, AC_BUILD_NAME " " AC_GIT_HASH "-" AC_BUILD_TS));# else JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, AC_BUILD_NAME " " AC_GIT_HASH));# endif# else JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, AC_BUILD_NAME));# endif#elif defined(AC_GIT_HASH)# ifdef AC_BUILD_TS JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, AC_GIT_HASH "-" AC_BUILD_TS));# else JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, AC_GIT_HASH));# endif#else JS_SetPropertyStr(ctx, sys, "version", JS_NewString(ctx, "unknown"));#endif
// 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));
// 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));
// 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)); 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 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);
// 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) { // 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 } } }
// 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;
for (int i = 0; i < input->event_count; i++) { JSValue api = build_api(rt->ctx, rt, "act"); 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);}