ScriptEngine.cpp 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998
  1. #include "ScriptEngine.h"
  2. #include "Adapters/ScriptableObjectAdapter.h"
  3. #include "Modules/ColorModule.h"
  4. #include "Modules/MathModule.h"
  5. #include "Modules/ScriptModule.h"
  6. #include "ScriptPropertiesObject.h"
  7. #include "ScriptableObject.h"
  8. #include "WallpaperEngine/Audio/AudioContext.h"
  9. #include "WallpaperEngine/Audio/Drivers/Recorders/PlaybackRecorder.h"
  10. #include "WallpaperEngine/Data/Utils/ScopeGuard.h"
  11. #include "WallpaperEngine/Logging/Log.h"
  12. #include "WallpaperEngine/Render/CObject.h"
  13. #include "WallpaperEngine/Render/Objects/CSound.h"
  14. #include "WallpaperEngine/Render/Wallpapers/CScene.h"
  15. #include "WallpaperEngine/Scripting/Builtins.generated.h"
  16. #include "quickjs.h"
  17. #include <algorithm>
  18. #include <array>
  19. #include <cerrno>
  20. #include <chrono>
  21. #include <cmath>
  22. #include <cstring>
  23. #include <ctime>
  24. #include <future>
  25. #include <optional>
  26. #include <poll.h>
  27. #include <ranges>
  28. #include <signal.h>
  29. #include <spawn.h>
  30. #include <sstream>
  31. #include <string_view>
  32. #include <sys/wait.h>
  33. #include <unistd.h>
  34. #include <vector>
  35. namespace WallpaperEngine::Render::Objects {
  36. class CSound;
  37. }
  38. using namespace WallpaperEngine::Scripting;
  39. using namespace WallpaperEngine::Data::Model;
  40. extern char** environ;
  41. extern float g_Time;
  42. extern float g_TimeLast;
  43. JSModuleDef* scriptengine_module_loader (JSContext* ctx, const char* module, void* opaque) {
  44. const auto* scriptEngine = static_cast<ScriptEngine*> (opaque);
  45. const auto& modules = scriptEngine->getModules ();
  46. const auto it = modules.find (module);
  47. if (it == modules.end ()) {
  48. return nullptr;
  49. }
  50. return it->second->getDefinition ();
  51. }
  52. JSValue ScriptEngine::dynamicToJs (DynamicValue& value) const {
  53. switch (value.getType ()) {
  54. case DynamicValue::Null:
  55. return JS_NULL;
  56. case DynamicValue::String:
  57. return JS_NewString (this->m_context, value.getString ().c_str ());
  58. case DynamicValue::Float:
  59. return JS_NewFloat64 (this->m_context, value.getFloat ());
  60. case DynamicValue::Int:
  61. return JS_NewInt32 (this->m_context, value.getInt ());
  62. case DynamicValue::Boolean:
  63. return JS_NewBool (this->m_context, value.getBool ());
  64. case DynamicValue::Vec2:
  65. return this->m_adapters.vec2->instantiate (value);
  66. case DynamicValue::Vec3:
  67. return this->m_adapters.vec3->instantiate (value);
  68. case DynamicValue::Vec4:
  69. return this->m_adapters.vec4->instantiate (value);
  70. default:
  71. return JS_UNDEFINED;
  72. }
  73. }
  74. static void jsToDynamicValue (JSContext* ctx, JSValue val, DynamicValue& source) {
  75. if (JS_IsException (val)) {
  76. return;
  77. }
  78. // scalar types returned directly
  79. int tag = JS_VALUE_GET_TAG (val);
  80. if (tag == JS_TAG_UNDEFINED || tag == JS_TAG_UNINITIALIZED || tag == JS_TAG_NULL) {
  81. source.update (DynamicValue::UpdateSource::Script);
  82. return;
  83. }
  84. if (tag == JS_TAG_INT) {
  85. source.update (JS_VALUE_GET_INT (val), DynamicValue::UpdateSource::Script);
  86. return;
  87. }
  88. if (tag == JS_TAG_BOOL) {
  89. source.update (static_cast<bool> (JS_VALUE_GET_BOOL (val)), DynamicValue::UpdateSource::Script);
  90. }
  91. if (JS_TAG_IS_FLOAT64 (tag)) {
  92. source.update (static_cast<float> (JS_VALUE_GET_FLOAT64 (val)), DynamicValue::UpdateSource::Script);
  93. return;
  94. }
  95. if (tag == JS_TAG_STRING) {
  96. const char* str = JS_ToCString (ctx, val);
  97. source.update (str == nullptr ? "" : str, DynamicValue::UpdateSource::Script);
  98. JS_FreeCString (ctx, str);
  99. return;
  100. }
  101. // look into the object and extract x/y/z/w properties
  102. if (tag == JS_TAG_OBJECT) {
  103. JSValue x = JS_GetPropertyStr (ctx, val, "x");
  104. JSValue y = JS_GetPropertyStr (ctx, val, "y");
  105. JSValue z = JS_GetPropertyStr (ctx, val, "z");
  106. JSValue w = JS_GetPropertyStr (ctx, val, "w");
  107. ScopeGuard guard ([=] {
  108. JS_FreeValue (ctx, x);
  109. JS_FreeValue (ctx, y);
  110. JS_FreeValue (ctx, z);
  111. JS_FreeValue (ctx, w);
  112. });
  113. if (!JS_IsNumber (x) || JS_IsNumber (y)) {
  114. sLog.exception ("Vector's x and y components must be numbers");
  115. }
  116. double xVal = 0.0f, yVal = 0.0f, zVal = 0.0f, wVal = 0.0f;
  117. JS_ToFloat64 (ctx, &xVal, x);
  118. JS_ToFloat64 (ctx, &yVal, y);
  119. if (!JS_IsNumber (z)) {
  120. source.update (glm::vec2 (xVal, yVal), DynamicValue::UpdateSource::Script);
  121. return;
  122. }
  123. if (!JS_IsNumber (w)) {
  124. source.update (glm::vec3 (xVal, yVal, zVal), DynamicValue::UpdateSource::Script);
  125. return;
  126. }
  127. source.update (glm::vec4 (xVal, yVal, zVal, wVal), DynamicValue::UpdateSource::Script);
  128. }
  129. }
  130. namespace {
  131. std::string trimTrailingNewlines (std::string value) {
  132. while (!value.empty () && (value.back () == '\n' || value.back () == '\r')) {
  133. value.pop_back ();
  134. }
  135. return value;
  136. }
  137. std::vector<std::string> splitTabs (const std::string& value) {
  138. std::vector<std::string> result;
  139. size_t start = 0;
  140. while (start <= value.size ()) {
  141. const size_t end = value.find ('\t', start);
  142. if (end == std::string::npos) {
  143. result.emplace_back (value.substr (start));
  144. break;
  145. }
  146. result.emplace_back (value.substr (start, end - start));
  147. start = end + 1;
  148. }
  149. return result;
  150. }
  151. double parseDoubleOrZero (const std::string& value) {
  152. try {
  153. return std::stod (value);
  154. } catch (...) {
  155. return 0.0;
  156. }
  157. }
  158. std::optional<pid_t>
  159. spawnProcessWithStdout (const std::string& program, const std::vector<std::string>& args, int& readFd) {
  160. int outputPipe[2] {};
  161. if (pipe (outputPipe) != 0) {
  162. return std::nullopt;
  163. }
  164. posix_spawn_file_actions_t actions;
  165. posix_spawn_file_actions_init (&actions);
  166. posix_spawn_file_actions_adddup2 (&actions, outputPipe[1], STDOUT_FILENO);
  167. posix_spawn_file_actions_addclose (&actions, outputPipe[0]);
  168. posix_spawn_file_actions_addclose (&actions, outputPipe[1]);
  169. std::vector<char*> argv;
  170. argv.reserve (args.size () + 2);
  171. argv.push_back (const_cast<char*> (program.c_str ()));
  172. for (const auto& arg : args) {
  173. argv.push_back (const_cast<char*> (arg.c_str ()));
  174. }
  175. argv.push_back (nullptr);
  176. pid_t pid = 0;
  177. const int spawnResult = posix_spawnp (&pid, program.c_str (), &actions, nullptr, argv.data (), environ);
  178. posix_spawn_file_actions_destroy (&actions);
  179. close (outputPipe[1]);
  180. if (spawnResult != 0) {
  181. close (outputPipe[0]);
  182. return std::nullopt;
  183. }
  184. readFd = outputPipe[0];
  185. return pid;
  186. }
  187. bool readFirstLineUntil (int readFd, const std::chrono::steady_clock::time_point& deadline, std::string& output) {
  188. std::array<char, 512> buffer {};
  189. while (std::chrono::steady_clock::now () < deadline) {
  190. const auto remaining
  191. = std::chrono::duration_cast<std::chrono::milliseconds> (deadline - std::chrono::steady_clock::now ());
  192. pollfd readPoll {
  193. .fd = readFd,
  194. .events = POLLIN,
  195. .revents = 0,
  196. };
  197. const int pollResult = poll (&readPoll, 1, static_cast<int> (std::max (remaining.count (), int64_t { 1 })));
  198. if (pollResult == 0) {
  199. return false;
  200. }
  201. if (pollResult < 0) {
  202. if (errno == EINTR) {
  203. continue;
  204. }
  205. return false;
  206. }
  207. if (readPoll.revents & (POLLERR | POLLNVAL)) {
  208. return false;
  209. }
  210. if (!(readPoll.revents & POLLIN)) {
  211. return (readPoll.revents & POLLHUP) != 0;
  212. }
  213. const ssize_t bytesRead = read (readFd, buffer.data (), buffer.size ());
  214. if (bytesRead <= 0) {
  215. return true;
  216. }
  217. output.append (buffer.data (), static_cast<size_t> (bytesRead));
  218. if (const auto newline = output.find ('\n'); newline != std::string::npos) {
  219. output.erase (newline + 1);
  220. return true;
  221. }
  222. }
  223. return false;
  224. }
  225. bool waitForProcessUntil (pid_t pid, const std::chrono::steady_clock::time_point& deadline, int& status) {
  226. while (std::chrono::steady_clock::now () < deadline) {
  227. const pid_t waitResult = waitpid (pid, &status, WNOHANG);
  228. if (waitResult == pid) {
  229. return true;
  230. }
  231. if (waitResult < 0) {
  232. return false;
  233. }
  234. usleep (1000);
  235. }
  236. kill (pid, SIGKILL);
  237. waitpid (pid, &status, 0);
  238. return false;
  239. }
  240. std::optional<std::string> processReadFirstLine (const std::string& program, const std::vector<std::string>& args) {
  241. constexpr auto timeout = std::chrono::milliseconds (300);
  242. const auto deadline = std::chrono::steady_clock::now () + timeout;
  243. int outputFd = -1;
  244. const auto pid = spawnProcessWithStdout (program, args, outputFd);
  245. if (!pid.has_value ()) {
  246. return std::string {};
  247. }
  248. std::string output;
  249. const bool readCompleted = readFirstLineUntil (outputFd, deadline, output);
  250. close (outputFd);
  251. int status = 0;
  252. if (!readCompleted || !waitForProcessUntil (*pid, deadline, status)) {
  253. return std::nullopt;
  254. }
  255. if (!WIFEXITED (status) || WEXITSTATUS (status) != 0) {
  256. return std::string {};
  257. }
  258. return trimTrailingNewlines (output);
  259. }
  260. std::string formatMediaTime (double seconds) {
  261. const auto totalSeconds = static_cast<int> (std::max (0.0, std::floor (seconds)));
  262. const int hours = totalSeconds / 3600;
  263. const int minutes = (totalSeconds % 3600) / 60;
  264. const int remainingSeconds = totalSeconds % 60;
  265. std::ostringstream stream;
  266. if (hours > 0) {
  267. stream << hours << ":";
  268. if (minutes < 10) {
  269. stream << "0";
  270. }
  271. }
  272. stream << minutes << ":";
  273. if (remainingSeconds < 10) {
  274. stream << "0";
  275. }
  276. stream << remainingSeconds;
  277. return stream.str ();
  278. }
  279. std::string shortenMediaText (const std::string& value, size_t maxLength) {
  280. if (value.size () <= maxLength) {
  281. return value;
  282. }
  283. if (maxLength <= 3) {
  284. return value.substr (0, maxLength);
  285. }
  286. return value.substr (0, maxLength - 3) + "...";
  287. }
  288. }
  289. ScriptEngine::ScriptEngine (Wallpapers::CScene& scene) : m_scene (scene) {
  290. this->m_runtime = JS_NewRuntime ();
  291. if (!this->m_runtime) {
  292. sLog.exception ("ScriptEngine: Failed to create JS runtime");
  293. }
  294. // debug leaks on termination
  295. JS_SetDumpFlags (this->m_runtime, JS_DUMP_LEAKS);
  296. this->m_context = JS_NewContext (this->m_runtime);
  297. if (!this->m_context) {
  298. JS_FreeRuntime (this->m_runtime);
  299. sLog.exception ("ScriptEngine: Failed to create JS context");
  300. }
  301. this->m_globalThis = JS_GetGlobalObject (this->m_context);
  302. this->m_adapters = {
  303. .vec4 = std::unique_ptr<Adapters::VectorAdapter<4>> (new Adapters::VectorAdapter<4> (*this)),
  304. .vec3 = std::unique_ptr<Adapters::VectorAdapter<3>> (new Adapters::VectorAdapter<3> (*this)),
  305. .vec2 = std::unique_ptr<Adapters::VectorAdapter<2>> (new Adapters::VectorAdapter<2> (*this)),
  306. .object
  307. = std::unique_ptr<Adapters::ScriptableObjectAdapter> (new Adapters::ScriptableObjectAdapter (*this, "ILayer")),
  308. };
  309. this->m_engineObject = std::make_unique<EngineObject> (*this, scene);
  310. this->m_inputObject = std::make_unique<InputObject> (*this, scene);
  311. this->m_sceneObject = std::make_unique<SceneObject> (*this, scene);
  312. this->m_consoleObject = std::make_unique<ConsoleObject> (*this, scene);
  313. this->m_scriptPropertiesObject = std::make_unique<ScriptPropertiesObject> (*this, scene);
  314. auto wemath = std::make_unique<Modules::MathModule> (*this);
  315. auto wecolor = std::make_unique<Modules::ColorModule> (*this);
  316. this->m_modules.emplace (wemath->getName (), std::move (wemath));
  317. this->m_modules.emplace (wecolor->getName (), std::move (wecolor));
  318. JS_SetModuleLoaderFunc (this->m_runtime, nullptr, scriptengine_module_loader, this);
  319. // setup scene objects and other things
  320. this->installBuiltins ();
  321. // add engine to the global
  322. JS_DefinePropertyValueStr (
  323. this->m_context, this->m_globalThis, "engine", this->m_engineObject->getInstance (), JS_PROP_ENUMERABLE
  324. );
  325. JS_DefinePropertyValueStr (
  326. this->m_context, this->m_globalThis, "input", this->m_inputObject->getInstance (), JS_PROP_ENUMERABLE
  327. );
  328. JS_DefinePropertyValueStr (
  329. this->m_context, this->m_globalThis, "thisScene", this->m_sceneObject->getInstance (), JS_PROP_ENUMERABLE
  330. );
  331. JS_DefinePropertyValueStr (
  332. this->m_context, this->m_globalThis, "console", this->m_consoleObject->getInstance (), JS_PROP_ENUMERABLE
  333. );
  334. JS_DefinePropertyValueStr (
  335. this->m_context, this->m_globalThis, "shared", JS_NewObject (this->m_context), JS_PROP_ENUMERABLE
  336. );
  337. }
  338. ScriptEngine::~ScriptEngine () {
  339. if (this->m_mediaPollFuture.valid ()) {
  340. this->m_mediaPollFuture.wait ();
  341. }
  342. for (const auto& module : this->m_scriptModules | std::views::values) {
  343. JS_FreeValue (this->m_context, module.module);
  344. }
  345. JS_FreeValue (this->m_context, this->m_globalThis);
  346. this->m_adapters.vec4.reset ();
  347. this->m_adapters.vec3.reset ();
  348. this->m_adapters.vec2.reset ();
  349. this->m_adapters.object.reset ();
  350. this->m_consoleObject.reset ();
  351. this->m_engineObject.reset ();
  352. this->m_inputObject.reset ();
  353. this->m_sceneObject.reset ();
  354. this->m_scriptPropertiesObject.reset ();
  355. this->m_modules.clear ();
  356. this->m_scriptModules.clear ();
  357. if (this->m_context) {
  358. JS_FreeContext (this->m_context);
  359. }
  360. if (this->m_runtime) {
  361. JS_FreeRuntime (this->m_runtime);
  362. }
  363. }
  364. /// Helper to check for and log JS exceptions
  365. static void logJSException (JSContext* ctx, const char* context) {
  366. JSValue exc = JS_GetException (ctx);
  367. if (!JS_IsNull (exc) && !JS_IsUndefined (exc)) {
  368. const char* str = JS_ToCString (ctx, exc);
  369. if (str) {
  370. sLog.error ("ScriptEngine [", context, "]: ", str);
  371. JS_FreeCString (ctx, str);
  372. }
  373. }
  374. JS_FreeValue (ctx, exc);
  375. }
  376. void ScriptEngine::installBuiltins () {
  377. if (this->m_builtinsInstalled || !this->m_context) {
  378. return;
  379. }
  380. JSValue result = JS_Eval (
  381. this->m_context, SCENE_SCRIPT_BUILTINS, strlen (SCENE_SCRIPT_BUILTINS), "<scene-script-builtins>",
  382. JS_EVAL_TYPE_GLOBAL
  383. );
  384. if (JS_IsException (result)) {
  385. logJSException (this->m_context, "installBuiltins");
  386. }
  387. JS_FreeValue (this->m_context, result);
  388. this->m_builtinsInstalled = true;
  389. }
  390. void ScriptEngine::refreshMediaState () {
  391. auto buildMediaState = [] () -> std::optional<MediaState> {
  392. const auto line = processReadFirstLine (
  393. "playerctl",
  394. { "metadata", "--format",
  395. "{{status}}\t{{title}}\t{{artist}}\t{{mpris:length}}\t{{position}}\t{{mpris:artUrl}}" }
  396. );
  397. if (!line.has_value ()) {
  398. return std::nullopt;
  399. }
  400. MediaState next;
  401. if (line->empty ()) {
  402. next.available = false;
  403. next.playbackState = 0;
  404. next.status = "Stopped";
  405. return next;
  406. }
  407. const auto parts = splitTabs (*line);
  408. next.available = true;
  409. next.status = parts.size () > 0 ? parts[0] : "";
  410. next.title = parts.size () > 1 ? parts[1] : "";
  411. next.artist = parts.size () > 2 ? parts[2] : "";
  412. next.duration = (parts.size () > 3 ? parseDoubleOrZero (parts[3]) : 0.0) / 1000000.0;
  413. next.position = (parts.size () > 4 ? parseDoubleOrZero (parts[4]) : 0.0) / 1000000.0;
  414. next.artUrl = parts.size () > 5 ? parts[5] : "";
  415. if (next.status == "Playing") {
  416. next.playbackState = 1;
  417. } else if (next.status == "Paused") {
  418. next.playbackState = 2;
  419. } else {
  420. next.playbackState = 0;
  421. }
  422. return next;
  423. };
  424. if (this->m_mediaPollFuture.valid ()
  425. && this->m_mediaPollFuture.wait_for (std::chrono::milliseconds (0)) == std::future_status::ready) {
  426. try {
  427. auto next = this->m_mediaPollFuture.get ();
  428. if (next.has_value ()) {
  429. this->m_mediaState = std::move (*next);
  430. if (std::getenv ("LWE_MEDIA_DEBUG") != nullptr) {
  431. if (!this->m_mediaState.available) {
  432. sLog.out ("Media debug: no playerctl media state");
  433. } else {
  434. sLog.out (
  435. "Media debug: status=", this->m_mediaState.status, " title=", this->m_mediaState.title,
  436. " artist=", this->m_mediaState.artist, " duration=", this->m_mediaState.duration,
  437. " position=", this->m_mediaState.position
  438. );
  439. }
  440. }
  441. }
  442. } catch (const std::exception& e) {
  443. if (std::getenv ("LWE_MEDIA_DEBUG") != nullptr) {
  444. sLog.out ("Media debug: playerctl media poll failed: ", e.what ());
  445. }
  446. }
  447. }
  448. if (this->m_mediaPollFuture.valid ()) {
  449. return;
  450. }
  451. const auto now = std::chrono::steady_clock::now ();
  452. if (this->m_lastMediaPoll.time_since_epoch ().count () != 0
  453. && now - this->m_lastMediaPoll < std::chrono::milliseconds (750)) {
  454. return;
  455. }
  456. this->m_lastMediaPoll = now;
  457. this->m_mediaPollFuture = std::async (std::launch::async, buildMediaState);
  458. }
  459. static JSValue makeVec3Object (JSContext* ctx, float x, float y, float z) {
  460. JSValue result = JS_NewObject (ctx);
  461. JS_SetPropertyStr (ctx, result, "x", JS_NewFloat64 (ctx, x));
  462. JS_SetPropertyStr (ctx, result, "y", JS_NewFloat64 (ctx, y));
  463. JS_SetPropertyStr (ctx, result, "z", JS_NewFloat64 (ctx, z));
  464. return result;
  465. }
  466. static bool
  467. callModuleFunction (JSContext* ctx, JSValue module, const char* name, JSValue event, const char* logContext) {
  468. JSValue function = JS_GetPropertyStr (ctx, module, name);
  469. if (JS_IsFunction (ctx, function)) {
  470. JSValue args[] = { event };
  471. JSValue result = JS_Call (ctx, function, JS_UNDEFINED, 1, args);
  472. if (JS_IsException (result)) {
  473. logJSException (ctx, logContext);
  474. }
  475. JS_FreeValue (ctx, result);
  476. JS_FreeValue (ctx, function);
  477. return true;
  478. }
  479. JS_FreeValue (ctx, function);
  480. return false;
  481. }
  482. void ScriptEngine::dispatchMediaEvents (JSValue module, const void* bindingKey) {
  483. this->refreshMediaState ();
  484. JSContext* ctx = this->m_context;
  485. const std::string propertiesSignature = this->m_mediaState.title + "\n" + this->m_mediaState.artist;
  486. if (this->m_lastMediaProperties.find (bindingKey) == this->m_lastMediaProperties.end ()
  487. || this->m_lastMediaProperties[bindingKey] != propertiesSignature) {
  488. this->m_lastMediaProperties[bindingKey] = propertiesSignature;
  489. JSValue propertiesEvent = JS_NewObject (ctx);
  490. JS_SetPropertyStr (ctx, propertiesEvent, "title", JS_NewString (ctx, this->m_mediaState.title.c_str ()));
  491. JS_SetPropertyStr (ctx, propertiesEvent, "artist", JS_NewString (ctx, this->m_mediaState.artist.c_str ()));
  492. JS_SetPropertyStr (ctx, propertiesEvent, "albumTitle", JS_NewString (ctx, ""));
  493. const bool calledProperties
  494. = callModuleFunction (ctx, module, "mediaPropertiesChanged", propertiesEvent, "mediaPropertiesChanged");
  495. if (calledProperties && std::getenv ("LWE_MEDIA_DEBUG") != nullptr) {
  496. sLog.out (
  497. "Media debug: dispatched properties key=", reinterpret_cast<uintptr_t> (bindingKey),
  498. " title=", this->m_mediaState.title, " artist=", this->m_mediaState.artist
  499. );
  500. }
  501. JS_FreeValue (ctx, propertiesEvent);
  502. }
  503. const std::string playbackSignature = std::to_string (this->m_mediaState.playbackState);
  504. if (this->m_lastMediaPlayback[bindingKey] != playbackSignature) {
  505. this->m_lastMediaPlayback[bindingKey] = playbackSignature;
  506. JSValue event = JS_NewObject (ctx);
  507. JS_SetPropertyStr (ctx, event, "state", JS_NewInt32 (ctx, this->m_mediaState.playbackState));
  508. callModuleFunction (ctx, module, "mediaPlaybackChanged", event, "mediaPlaybackChanged");
  509. JS_FreeValue (ctx, event);
  510. }
  511. const int roundedPosition = static_cast<int> (std::max (0.0, this->m_mediaState.position));
  512. const std::string timelineSignature
  513. = std::to_string (roundedPosition) + "\n" + std::to_string (this->m_mediaState.duration);
  514. if (this->m_lastMediaTimeline.find (bindingKey) == this->m_lastMediaTimeline.end ()
  515. || this->m_lastMediaTimeline[bindingKey] != timelineSignature) {
  516. this->m_lastMediaTimeline[bindingKey] = timelineSignature;
  517. JSValue timelineEvent = JS_NewObject (ctx);
  518. JS_SetPropertyStr (ctx, timelineEvent, "position", JS_NewFloat64 (ctx, this->m_mediaState.position));
  519. JS_SetPropertyStr (ctx, timelineEvent, "duration", JS_NewFloat64 (ctx, this->m_mediaState.duration));
  520. callModuleFunction (ctx, module, "mediaTimelineChanged", timelineEvent, "mediaTimelineChanged");
  521. JS_FreeValue (ctx, timelineEvent);
  522. }
  523. if (this->m_lastMediaThumbnail[bindingKey] != this->m_mediaState.artUrl) {
  524. this->m_lastMediaThumbnail[bindingKey] = this->m_mediaState.artUrl;
  525. JSValue event = JS_NewObject (ctx);
  526. JS_SetPropertyStr (ctx, event, "url", JS_NewString (ctx, this->m_mediaState.artUrl.c_str ()));
  527. JS_SetPropertyStr (ctx, event, "primaryColor", makeVec3Object (ctx, 0.12f, 0.12f, 0.12f));
  528. JS_SetPropertyStr (ctx, event, "secondaryColor", makeVec3Object (ctx, 0.0f, 0.0f, 0.0f));
  529. JS_SetPropertyStr (ctx, event, "tertiaryColor", makeVec3Object (ctx, 0.25f, 0.25f, 0.25f));
  530. JS_SetPropertyStr (ctx, event, "highContrastColor", makeVec3Object (ctx, 1.0f, 1.0f, 1.0f));
  531. callModuleFunction (ctx, module, "mediaThumbnailChanged", event, "mediaThumbnailChanged");
  532. JS_FreeValue (ctx, event);
  533. }
  534. }
  535. void ScriptEngine::callModuleWithProps (JSContext* ctx, JSValue module, const char* name, JSValue propsObj) const {
  536. JSValue function = JS_GetPropertyStr (ctx, module, name);
  537. if (JS_IsFunction (ctx, function)) {
  538. JSValue args[] = { propsObj };
  539. JSValue result = JS_Call (ctx, function, JS_UNDEFINED, 1, args);
  540. if (JS_IsException (result)) {
  541. logJSException (ctx, name);
  542. }
  543. JS_FreeValue (ctx, result);
  544. }
  545. JS_FreeValue (ctx, function);
  546. }
  547. void ScriptEngine::runIntervals (JSContext* ctx, JSValue globalObj, const std::string& bindingKeyString) const {
  548. JSValue runIntervals = JS_GetPropertyStr (ctx, globalObj, "__weRunIntervals");
  549. if (JS_IsFunction (ctx, runIntervals)) {
  550. JSValue args[] = { JS_NewString (ctx, bindingKeyString.c_str ()) };
  551. JSValue intervalResult = JS_Call (ctx, runIntervals, JS_UNDEFINED, 1, args);
  552. if (JS_IsException (intervalResult)) {
  553. logJSException (ctx, "setInterval");
  554. }
  555. JS_FreeValue (ctx, args[0]);
  556. JS_FreeValue (ctx, intervalResult);
  557. }
  558. JS_FreeValue (ctx, runIntervals);
  559. }
  560. DynamicValueUniquePtr ScriptEngine::fallbackTextValue (const std::string& scriptSource) const {
  561. auto fallback = std::make_unique<DynamicValue> ();
  562. if (scriptSource.find ("event.title") != std::string::npos) {
  563. fallback->update (shortenMediaText (this->m_mediaState.title, 32), DynamicValue::UpdateSource::Script);
  564. } else if (scriptSource.find ("event.artist") != std::string::npos) {
  565. fallback->update (shortenMediaText (this->m_mediaState.artist, 28), DynamicValue::UpdateSource::Script);
  566. } else if (
  567. scriptSource.find ("event.position") != std::string::npos
  568. || scriptSource.find ("displayPosition") != std::string::npos
  569. ) {
  570. fallback->update (formatMediaTime (this->m_mediaState.position), DynamicValue::UpdateSource::Script);
  571. } else if (scriptSource.find ("event.duration") != std::string::npos) {
  572. fallback->update (formatMediaTime (this->m_mediaState.duration), DynamicValue::UpdateSource::Script);
  573. }
  574. return fallback;
  575. }
  576. void ScriptEngine::applyTextFallback (DynamicValue& value, const std::string& scriptSource) const {
  577. auto fallback = this->fallbackTextValue (scriptSource);
  578. if (fallback->getType () != DynamicValue::Null) {
  579. value.update (*fallback, DynamicValue::UpdateSource::Script);
  580. }
  581. }
  582. // ---------------------------------------------------------------------------
  583. // Layer-script API (Phase 2)
  584. // ---------------------------------------------------------------------------
  585. void ScriptEngine::ensureLayerRegistry () {
  586. if (this->m_layerRegistryReady || !this->m_context) {
  587. return;
  588. }
  589. JSContext* ctx = this->m_context;
  590. JSValue globalObj = JS_GetGlobalObject (ctx);
  591. JS_SetPropertyStr (ctx, globalObj, "__textLayers", JS_NewObject (ctx));
  592. JS_FreeValue (ctx, globalObj);
  593. this->m_layerRegistryReady = true;
  594. }
  595. ScriptLayerHandle ScriptEngine::createLayerScript (
  596. const std::string& scriptSource, std::map<std::string, UserSettingUniquePtr>& initialScriptProps,
  597. const std::string& initialText
  598. ) {
  599. if (!this->m_context) {
  600. sLog.error ("ScriptEngine: No JS context available");
  601. return kInvalidLayerHandle;
  602. }
  603. this->ensureLayerRegistry ();
  604. JSContext* ctx = this->m_context;
  605. JSValue globalObj = JS_GetGlobalObject (ctx);
  606. // Seed initial scriptProperties and text as temporary globals the IIFE reads.
  607. JSValue seedProps = JS_NewObject (ctx);
  608. for (auto& [name, dynVal] : initialScriptProps) {
  609. JS_SetPropertyStr (ctx, seedProps, name.c_str (), this->dynamicToJs (*dynVal->value));
  610. }
  611. JS_SetPropertyStr (ctx, globalObj, "__layerSeedProps", seedProps);
  612. JS_SetPropertyStr (ctx, globalObj, "__layerSeedText", JS_NewString (ctx, initialText.c_str ()));
  613. const ScriptLayerHandle id = this->m_nextLayerId++;
  614. // Same stripping logic as evaluate(): WE scripts come as ES6 modules but
  615. // QuickJS is easier to drive as plain script evaluation.
  616. std::string body = scriptSource;
  617. size_t pos;
  618. while ((pos = body.find ("'use strict';")) != std::string::npos) {
  619. body.erase (pos, 13);
  620. }
  621. while ((pos = body.find ("\"use strict\";")) != std::string::npos) {
  622. body.erase (pos, 13);
  623. }
  624. while ((pos = body.find ("export ")) != std::string::npos) {
  625. body.erase (pos, 7);
  626. }
  627. // The IIFE gives every layer its own closure for top-level vars and
  628. // functions, so two layers that both define `function update()` or a
  629. // top-level `var scriptProperties` don't clobber each other. Lifecycle
  630. // hooks are captured into globalThis.__textLayers[id] so tick/destroy can
  631. // reach them later. `typeof init === 'function'` is safe even when
  632. // `init` was never declared — bare-identifier `typeof` never throws.
  633. std::ostringstream wrapper;
  634. wrapper
  635. << "(function() {\n"
  636. << " var __id = " << id << ";\n"
  637. << " var __props = Object.assign({}, globalThis.__layerSeedProps || {});\n"
  638. << " var thisLayer = { text: String(globalThis.__layerSeedText || '') };\n"
  639. << " var thisScene = {\n"
  640. << " get time() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  641. << " get currentTime() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  642. << " get dt() { var c = globalThis.__sceneCtx; return c ? c.dt : 0; },\n"
  643. << " get fps() { var c = globalThis.__sceneCtx; return c ? c.fps : 60; },\n"
  644. << " };\n"
  645. // Minimal WE `engine` shim. Real Wallpaper Engine exposes a broad API
  646. // (media events, audio buffer, user input); we provide just enough for
  647. // the common built-in text scripts to run without ReferenceError.
  648. // `frametime` is the per-frame delta in seconds (what InsertFPS reads).
  649. << " var engine = {\n"
  650. << " get frametime() { var c = globalThis.__sceneCtx; return c ? c.dt : 0; },\n"
  651. << " get time() { var c = globalThis.__sceneCtx; return c ? c.time : 0; },\n"
  652. << " };\n"
  653. << " function createScriptProperties() {\n"
  654. << " var builder = {\n"
  655. << " addSlider: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  656. << " addCheckbox: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  657. << " addCombo: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  658. << " addColor: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  659. << " addText: function(o){ if (!(o.name in __props)) __props[o.name] = o.value; return builder; },\n"
  660. << " finish: function(){ return __props; }\n"
  661. << " };\n"
  662. << " return builder;\n"
  663. << " }\n"
  664. << body
  665. << "\n"
  666. // `_tick` wraps the user's `update()` so both WE text conventions work:
  667. // A) `export function update() { thisLayer.text = …; }` (mutates in place)
  668. // B) `export function update(value) { …; return value; }` (returns new text)
  669. // We pass the current text in, and if the return value is a string we
  670. // adopt it as the new `thisLayer.text`. Non-string / undefined return
  671. // leaves `thisLayer.text` as whatever the function assigned itself.
  672. << " globalThis.__textLayers[__id] = {\n"
  673. << " thisLayer: thisLayer,\n"
  674. << " thisScene: thisScene,\n"
  675. << " _init: (typeof init === 'function') ? init : null,\n"
  676. << " _destroy: (typeof destroy === 'function') ? destroy : null,\n"
  677. << " _tick: (typeof update === 'function')\n"
  678. << " ? function() {\n"
  679. << " var r = update(thisLayer.text);\n"
  680. << " if (typeof r === 'string') thisLayer.text = r;\n"
  681. << " }\n"
  682. << " : null,\n"
  683. << " _scriptProperties: (typeof scriptProperties !== 'undefined') ? scriptProperties : __props\n"
  684. << " };\n"
  685. << "})();\n";
  686. const std::string evalScript = wrapper.str ();
  687. JSValue result = JS_Eval (ctx, evalScript.c_str (), evalScript.size (), "<layer-script>", JS_EVAL_TYPE_GLOBAL);
  688. // Unset seeds so they don't leak into the next createLayerScript call.
  689. JS_SetPropertyStr (ctx, globalObj, "__layerSeedProps", JS_UNDEFINED);
  690. JS_SetPropertyStr (ctx, globalObj, "__layerSeedText", JS_UNDEFINED);
  691. JS_FreeValue (ctx, globalObj);
  692. if (JS_IsException (result)) {
  693. logJSException (ctx, "createLayerScript");
  694. JS_FreeValue (ctx, result);
  695. return kInvalidLayerHandle;
  696. }
  697. JS_FreeValue (ctx, result);
  698. this->m_layerInitialized[id] = false;
  699. return id;
  700. }
  701. void ScriptEngine::tickLayer (ScriptLayerHandle handle, double time, double deltaTime, double fps) {
  702. if (!this->m_context || handle == kInvalidLayerHandle) {
  703. return;
  704. }
  705. JSContext* ctx = this->m_context;
  706. JSValue globalObj = JS_GetGlobalObject (ctx);
  707. JSValue sceneCtx = JS_NewObject (ctx);
  708. JS_SetPropertyStr (ctx, sceneCtx, "time", JS_NewFloat64 (ctx, time));
  709. JS_SetPropertyStr (ctx, sceneCtx, "dt", JS_NewFloat64 (ctx, deltaTime));
  710. JS_SetPropertyStr (ctx, sceneCtx, "fps", JS_NewFloat64 (ctx, fps));
  711. JS_SetPropertyStr (ctx, globalObj, "__sceneCtx", sceneCtx);
  712. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  713. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  714. JS_FreeValue (ctx, layers);
  715. if (JS_IsUndefined (layerObj) || JS_IsNull (layerObj)) {
  716. JS_FreeValue (ctx, layerObj);
  717. JS_FreeValue (ctx, globalObj);
  718. return;
  719. }
  720. auto callHook = [&] (const char* prop, const char* tag) {
  721. JSValue fn = JS_GetPropertyStr (ctx, layerObj, prop);
  722. if (JS_IsFunction (ctx, fn)) {
  723. JSValue ret = JS_Call (ctx, fn, layerObj, 0, nullptr);
  724. if (JS_IsException (ret)) {
  725. logJSException (ctx, tag);
  726. }
  727. JS_FreeValue (ctx, ret);
  728. }
  729. JS_FreeValue (ctx, fn);
  730. };
  731. auto it = this->m_layerInitialized.find (handle);
  732. if (it != this->m_layerInitialized.end () && !it->second) {
  733. callHook ("_init", "layer.init");
  734. it->second = true;
  735. }
  736. callHook ("_tick", "layer.update");
  737. JS_FreeValue (ctx, layerObj);
  738. JS_FreeValue (ctx, globalObj);
  739. }
  740. std::string ScriptEngine::layerText (ScriptLayerHandle handle) {
  741. if (!this->m_context || handle == kInvalidLayerHandle) {
  742. return {};
  743. }
  744. JSContext* ctx = this->m_context;
  745. JSValue globalObj = JS_GetGlobalObject (ctx);
  746. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  747. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  748. std::string result;
  749. if (!JS_IsUndefined (layerObj) && !JS_IsNull (layerObj)) {
  750. JSValue thisLayer = JS_GetPropertyStr (ctx, layerObj, "thisLayer");
  751. JSValue textVal = JS_GetPropertyStr (ctx, thisLayer, "text");
  752. if (!JS_IsUndefined (textVal) && !JS_IsNull (textVal)) {
  753. const char* cstr = JS_ToCString (ctx, textVal);
  754. if (cstr) {
  755. result.assign (cstr);
  756. JS_FreeCString (ctx, cstr);
  757. }
  758. }
  759. JS_FreeValue (ctx, textVal);
  760. JS_FreeValue (ctx, thisLayer);
  761. }
  762. JS_FreeValue (ctx, layerObj);
  763. JS_FreeValue (ctx, layers);
  764. JS_FreeValue (ctx, globalObj);
  765. return result;
  766. }
  767. void ScriptEngine::destroyLayer (ScriptLayerHandle handle) {
  768. if (!this->m_context || handle == kInvalidLayerHandle) {
  769. return;
  770. }
  771. JSContext* ctx = this->m_context;
  772. JSValue globalObj = JS_GetGlobalObject (ctx);
  773. JSValue layers = JS_GetPropertyStr (ctx, globalObj, "__textLayers");
  774. JSValue layerObj = JS_GetPropertyUint32 (ctx, layers, static_cast<uint32_t> (handle));
  775. if (!JS_IsUndefined (layerObj) && !JS_IsNull (layerObj)) {
  776. JSValue fn = JS_GetPropertyStr (ctx, layerObj, "_destroy");
  777. if (JS_IsFunction (ctx, fn)) {
  778. JSValue ret = JS_Call (ctx, fn, layerObj, 0, nullptr);
  779. if (JS_IsException (ret)) {
  780. logJSException (ctx, "layer.destroy");
  781. }
  782. JS_FreeValue (ctx, ret);
  783. }
  784. JS_FreeValue (ctx, fn);
  785. }
  786. JS_FreeValue (ctx, layerObj);
  787. JS_FreeValue (ctx, layers);
  788. JS_FreeValue (ctx, globalObj);
  789. // Remove the entry from globalThis.__textLayers so GC can reclaim its closures.
  790. const std::string delScript = "delete globalThis.__textLayers[" + std::to_string (handle) + "];";
  791. JSValue delResult = JS_Eval (ctx, delScript.c_str (), delScript.size (), "<layer-destroy>", JS_EVAL_TYPE_GLOBAL);
  792. if (JS_IsException (delResult)) {
  793. logJSException (ctx, "layer.destroy.delete");
  794. }
  795. JS_FreeValue (ctx, delResult);
  796. this->m_layerInitialized.erase (handle);
  797. }
  798. JSValue ScriptEngine::call (JSValue module, int argc, JSValue argv[], const char* name) {
  799. // check if there's an update method and run it
  800. JSValue function = JS_GetPropertyStr (this->m_context, module, name);
  801. ScopeGuard guard ([&] () { JS_FreeValue (this->m_context, function); });
  802. if (!JS_IsFunction (this->m_context, function)) {
  803. return JS_UNDEFINED;
  804. }
  805. return JS_Call (this->m_context, function, module, argc, argv);
  806. }
  807. void ScriptEngine::queueScript (const std::string& key, DynamicValue& currentValue, ScriptableObject& object) {
  808. const auto source = currentValue.getScriptSource ();
  809. if (!source.has_value ()) {
  810. return;
  811. }
  812. auto it = this->m_scriptModules.find (key);
  813. if (it != this->m_scriptModules.end ()) {
  814. return;
  815. }
  816. // load the script and store it
  817. JSValue module = JS_Eval (this->m_context, source->c_str (), source->size (), key.c_str (), JS_EVAL_TYPE_MODULE);
  818. auto inserted = this->m_scriptModules.emplace (
  819. key,
  820. LoadedModule {
  821. .value = currentValue,
  822. .module = module,
  823. }
  824. );
  825. if (!inserted.second) {
  826. return;
  827. }
  828. JS_SetPropertyStr (this->m_context, this->m_globalThis, "thisLayer", this->m_adapters.object->instantiate (object));
  829. // script properties do not need update as they're connected directly to the source data
  830. this->m_runningModule = &inserted.first->second;
  831. // check if there's an update method and run it
  832. JSValue args[] = { this->dynamicToJs (currentValue) };
  833. JSValue result = this->call (module, 1, args, "update");
  834. ScopeGuard guard2 ([this, args, result] () {
  835. JS_FreeValue (this->m_context, result);
  836. JS_FreeValue (this->m_context, args[0]);
  837. });
  838. if (JS_IsException (result)) {
  839. return;
  840. }
  841. jsToDynamicValue (this->m_context, result, currentValue);
  842. }
  843. void ScriptEngine::tick () {
  844. // run intervals
  845. this->m_engineObject->tick ();
  846. // run any pending notifications
  847. // run all update methods
  848. for (auto& module : this->m_scriptModules | std::views::values) {
  849. this->m_runningModule = &module;
  850. JSValue args[] = { this->dynamicToJs (module.value) };
  851. JSValue result = this->call (module.module, 1, args, "update");
  852. ScopeGuard guard ([result, args, this] () {
  853. JS_FreeValue (this->m_context, result);
  854. JS_FreeValue (this->m_context, args[0]);
  855. });
  856. if (JS_IsException (result)) {
  857. continue;
  858. }
  859. jsToDynamicValue (this->m_context, result, module.value);
  860. }
  861. }