summaryrefslogtreecommitdiff
path: root/src/tangara/lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/tangara/lua')
-rw-r--r--src/tangara/lua/bridge.cpp144
-rw-r--r--src/tangara/lua/bridge.hpp53
-rw-r--r--src/tangara/lua/lua_controls.cpp58
-rw-r--r--src/tangara/lua/lua_controls.hpp15
-rw-r--r--src/tangara/lua/lua_database.cpp304
-rw-r--r--src/tangara/lua/lua_database.hpp19
-rw-r--r--src/tangara/lua/lua_queue.cpp74
-rw-r--r--src/tangara/lua/lua_queue.hpp15
-rw-r--r--src/tangara/lua/lua_registry.hpp51
-rw-r--r--src/tangara/lua/lua_screen.cpp79
-rw-r--r--src/tangara/lua/lua_screen.hpp15
-rw-r--r--src/tangara/lua/lua_theme.cpp92
-rw-r--r--src/tangara/lua/lua_theme.hpp15
-rw-r--r--src/tangara/lua/lua_thread.cpp189
-rw-r--r--src/tangara/lua/lua_thread.hpp44
-rw-r--r--src/tangara/lua/lua_version.cpp68
-rw-r--r--src/tangara/lua/lua_version.hpp15
-rw-r--r--src/tangara/lua/property.cpp460
-rw-r--r--src/tangara/lua/property.hpp107
-rw-r--r--src/tangara/lua/registry.cpp73
20 files changed, 1890 insertions, 0 deletions
diff --git a/src/tangara/lua/bridge.cpp b/src/tangara/lua/bridge.cpp
new file mode 100644
index 00000000..07c299a7
--- /dev/null
+++ b/src/tangara/lua/bridge.cpp
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/bridge.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "database/database.hpp"
+#include "database/index.hpp"
+#include "esp_log.h"
+#include "lauxlib.h"
+#include "lua.h"
+#include "lua.hpp"
+#include "lua/lua_controls.hpp"
+#include "lua/lua_database.hpp"
+#include "lua/lua_queue.hpp"
+#include "lua/lua_screen.hpp"
+#include "lua/lua_theme.hpp"
+#include "lua/lua_version.hpp"
+#include "lvgl.h"
+
+#include "font/lv_font_loader.h"
+#include "luavgl.h"
+
+#include "events/event_queue.hpp"
+#include "lua/property.hpp"
+#include "system_fsm/service_locator.hpp"
+#include "ui/ui_events.hpp"
+
+extern "C" {
+int luaopen_linenoise(lua_State* L);
+int luaopen_term_core(lua_State* L);
+}
+
+LV_FONT_DECLARE(font_fusion_12);
+LV_FONT_DECLARE(font_fusion_10);
+
+namespace lua {
+
+[[maybe_unused]] static constexpr char kTag[] = "lua_bridge";
+
+static constexpr char kBridgeKey[] = "bridge";
+
+static auto make_font_cb(const char* name,
+ int size,
+ int weight) -> const lv_font_t* {
+ if (std::string{"fusion"} == name) {
+ if (size == 12) {
+ return &font_fusion_12;
+ }
+ if (size == 10) {
+ return &font_fusion_10;
+ }
+ }
+ return NULL;
+}
+
+static auto delete_font_cb(lv_font_t* font) -> void {}
+
+auto Bridge::Get(lua_State* state) -> Bridge* {
+ lua_pushstring(state, kBridgeKey);
+ lua_gettable(state, LUA_REGISTRYINDEX);
+ return reinterpret_cast<Bridge*>(lua_touserdata(state, -1));
+}
+
+Bridge::Bridge(system_fsm::ServiceLocator& services) : services_(services) {}
+
+auto Bridge::installBaseModules(lua_State* L) -> void {
+ lua_pushstring(L, kBridgeKey);
+ lua_pushlightuserdata(L, this);
+ lua_settable(L, LUA_REGISTRYINDEX);
+
+ bindings_.install(L);
+
+ luaL_requiref(L, "linenoise", luaopen_linenoise, true);
+ lua_pop(L, 1);
+
+ luaL_requiref(L, "term.core", luaopen_term_core, true);
+ lua_pop(L, 1);
+
+ RegisterControlsModule(L);
+ RegisterDatabaseModule(L);
+ RegisterQueueModule(L);
+ RegisterVersionModule(L);
+ RegisterThemeModule(L);
+ RegisterScreenModule(L);
+}
+
+auto Bridge::installLvgl(lua_State* L) -> void {
+ luavgl_set_pcall(L, CallProtected);
+ luavgl_set_font_extension(L, make_font_cb, delete_font_cb);
+ luaL_requiref(L, "lvgl", luaopen_lvgl, true);
+ lua_pop(L, 1);
+}
+
+static auto new_property_module(lua_State* state) -> int {
+ const char* name = luaL_checkstring(state, 1);
+ luaL_newmetatable(state, name);
+
+ lua_pushstring(state, "__index");
+ lua_pushvalue(state, -2);
+ lua_settable(state, -3); // metatable.__index = metatable
+
+ return 1;
+}
+
+template <class... Ts>
+inline constexpr bool always_false_v = false;
+
+auto Bridge::installPropertyModule(
+ lua_State* L,
+ const std::string& name,
+ std::vector<std::pair<std::string, std::variant<LuaFunction, Property*>>>&
+ props) -> void {
+ // Create the module (or retrieve it if one with this name already exists)
+ luaL_requiref(L, name.c_str(), new_property_module, true);
+
+ for (const auto& prop : props) {
+ lua_pushstring(L, prop.first.c_str());
+ std::visit(
+ [&](auto&& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, LuaFunction>) {
+ bindings_.Register(L, arg);
+ } else if constexpr (std::is_same_v<T, Property*>) {
+ bindings_.Register(L, arg);
+ } else {
+ static_assert(always_false_v<T>, "missing case");
+ }
+ },
+ prop.second);
+
+ lua_settable(L, -3); // metatable.propname = property
+ }
+
+ lua_pop(L, 1); // pop the module off the stack
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/bridge.hpp b/src/tangara/lua/bridge.hpp
new file mode 100644
index 00000000..b4cfe503
--- /dev/null
+++ b/src/tangara/lua/bridge.hpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "lua.hpp"
+#include "lua/property.hpp"
+#include "lvgl.h"
+#include "system_fsm/service_locator.hpp"
+
+namespace lua {
+
+/*
+ * Responsible for adding C/C++ module bindings to Lua threads. This class
+ * keeps no thread-specific internal state, and instead uses the LUA_REGISTRY
+ * table of its host threads to store data.
+ */
+class Bridge {
+ public:
+ /*
+ * Utility for retrieving the Bridge from a Lua thread in which the Bridge's
+ * bindings have been installed. Use by Lua's C callbacks to access the rest
+ * of the system.
+ */
+ static auto Get(lua_State* state) -> Bridge*;
+
+ Bridge(system_fsm::ServiceLocator& s);
+
+ system_fsm::ServiceLocator& services() { return services_; }
+
+ auto installBaseModules(lua_State* L) -> void;
+ auto installLvgl(lua_State* L) -> void;
+ auto installPropertyModule(
+ lua_State* L,
+ const std::string&,
+ std::vector<std::pair<std::string,
+ std::variant<LuaFunction, Property*>>>&) -> void;
+
+ Bridge(const Bridge&) = delete;
+ Bridge& operator=(const Bridge&) = delete;
+
+ private:
+ system_fsm::ServiceLocator& services_;
+ PropertyBindings bindings_;
+};
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_controls.cpp b/src/tangara/lua/lua_controls.cpp
new file mode 100644
index 00000000..baf40891
--- /dev/null
+++ b/src/tangara/lua/lua_controls.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/lua_controls.hpp"
+
+#include <memory>
+#include <string>
+
+#include "lua.hpp"
+
+#include "esp_log.h"
+#include "lauxlib.h"
+#include "lua.h"
+#include "lvgl.h"
+
+#include "drivers/nvs.hpp"
+#include "ui/ui_events.hpp"
+
+namespace lua {
+
+[[maybe_unused]] static constexpr char kTag[] = "lua_controls";
+
+static auto controls_schemes(lua_State* L) -> int {
+ lua_newtable(L);
+
+ lua_pushliteral(L, "Buttons Only");
+ lua_rawseti(L, -2,
+ static_cast<int>(drivers::NvsStorage::InputModes::kButtonsOnly));
+
+ lua_pushliteral(L, "D-Pad");
+ lua_rawseti(
+ L, -2,
+ static_cast<int>(drivers::NvsStorage::InputModes::kDirectionalWheel));
+
+ lua_pushliteral(L, "Touchwheel");
+ lua_rawseti(
+ L, -2, static_cast<int>(drivers::NvsStorage::InputModes::kRotatingWheel));
+
+ return 1;
+}
+
+static const struct luaL_Reg kControlsFuncs[] = {{"schemes", controls_schemes},
+ {NULL, NULL}};
+
+static auto lua_controls(lua_State* state) -> int {
+ luaL_newlib(state, kControlsFuncs);
+ return 1;
+}
+
+auto RegisterControlsModule(lua_State* s) -> void {
+ luaL_requiref(s, "controls", lua_controls, true);
+ lua_pop(s, 1);
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_controls.hpp b/src/tangara/lua/lua_controls.hpp
new file mode 100644
index 00000000..18ad261d
--- /dev/null
+++ b/src/tangara/lua/lua_controls.hpp
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include "lua.hpp"
+
+namespace lua {
+
+auto RegisterControlsModule(lua_State*) -> void;
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_database.cpp b/src/tangara/lua/lua_database.cpp
new file mode 100644
index 00000000..1afb01f0
--- /dev/null
+++ b/src/tangara/lua/lua_database.cpp
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/lua_database.hpp"
+
+#include <memory>
+#include <string>
+#include <type_traits>
+#include <variant>
+
+#include "lua.hpp"
+#include "lua/bridge.hpp"
+
+#include "esp_log.h"
+#include "lauxlib.h"
+#include "lua.h"
+#include "lua/lua_thread.hpp"
+#include "lvgl.h"
+
+#include "database/database.hpp"
+#include "database/index.hpp"
+#include "database/records.hpp"
+#include "database/track.hpp"
+#include "events/event_queue.hpp"
+#include "lua/property.hpp"
+#include "system_fsm/service_locator.hpp"
+#include "ui/ui_events.hpp"
+
+namespace lua {
+
+[[maybe_unused]] static constexpr char kTag[] = "lua_db";
+
+static constexpr char kDbIndexMetatable[] = "db_index";
+static constexpr char kDbRecordMetatable[] = "db_record";
+static constexpr char kDbIteratorMetatable[] = "db_iterator";
+
+struct LuaIndexInfo {
+ database::IndexId id;
+ size_t name_size;
+ char name_data[];
+
+ auto name() -> std::string_view { return {name_data, name_size}; }
+};
+
+static_assert(std::is_trivially_destructible<LuaIndexInfo>());
+static_assert(std::is_trivially_copy_assignable<LuaIndexInfo>());
+
+static auto indexes(lua_State* state) -> int {
+ Bridge* instance = Bridge::Get(state);
+
+ lua_newtable(state);
+
+ auto db = instance->services().database().lock();
+ if (!db) {
+ return 1;
+ }
+
+ for (const auto& i : db->getIndexes()) {
+ LuaIndexInfo* data = reinterpret_cast<LuaIndexInfo*>(
+ lua_newuserdata(state, sizeof(LuaIndexInfo) + i.name.size()));
+ luaL_setmetatable(state, kDbIndexMetatable);
+ *data = LuaIndexInfo{
+ .id = i.id,
+ .name_size = i.name.size(),
+ };
+ std::memcpy(data->name_data, i.name.data(), i.name.size());
+ lua_rawseti(state, -2, i.id);
+ }
+
+ return 1;
+}
+
+static auto version(lua_State* L) -> int {
+ Bridge* instance = Bridge::Get(L);
+ auto db = instance->services().database().lock();
+ if (!db) {
+ return 0;
+ }
+ auto res = db->schemaVersion();
+ lua_pushlstring(L, res.data(), res.size());
+ return 1;
+}
+
+static auto size(lua_State* L) -> int {
+ Bridge* instance = Bridge::Get(L);
+ auto db = instance->services().database().lock();
+ if (!db) {
+ return 0;
+ }
+ lua_pushinteger(L, db->sizeOnDiskBytes());
+ return 1;
+}
+
+static auto recreate(lua_State* L) -> int {
+ ESP_LOGI(kTag, "recreate");
+ return 0;
+}
+
+static auto update(lua_State* L) -> int {
+ Bridge* instance = Bridge::Get(L);
+ auto db = instance->services().database().lock();
+ if (!db) {
+ return 0;
+ }
+
+ instance->services().bg_worker().Dispatch<void>(
+ [=]() { db->updateIndexes(); });
+ return 0;
+}
+
+static const struct luaL_Reg kDatabaseFuncs[] = {
+ {"indexes", indexes}, {"version", version}, {"size", size},
+ {"recreate", recreate}, {"update", update}, {NULL, NULL}};
+
+/*
+ * Struct to be used as userdata for the Lua representation of database records.
+ * In order to push these large values into PSRAM as much as possible, memory
+ * for these is allocated and managed by Lua. This struct must therefore be
+ * trivially copyable.
+ */
+struct LuaRecord {
+ std::variant<database::TrackId, database::IndexKey::Header> contents;
+ size_t text_size;
+ char text[];
+};
+
+static_assert(std::is_trivially_destructible<LuaRecord>());
+static_assert(std::is_trivially_copy_assignable<LuaRecord>());
+
+static auto push_lua_record(lua_State* L, const database::Record& r) -> void {
+ // Create and init the userdata.
+ LuaRecord* record = reinterpret_cast<LuaRecord*>(
+ lua_newuserdata(L, sizeof(LuaRecord) + r.text().size()));
+ luaL_setmetatable(L, kDbRecordMetatable);
+
+ // Init all the fields
+ *record = {
+ .contents = r.contents(),
+ .text_size = r.text().size(),
+ };
+
+ // Copy the string data across.
+ std::memcpy(record->text, r.text().data(), r.text().size());
+}
+
+auto db_check_iterator(lua_State* L, int stack_pos) -> database::Iterator* {
+ database::Iterator* it = *reinterpret_cast<database::Iterator**>(
+ luaL_checkudata(L, stack_pos, kDbIteratorMetatable));
+ return it;
+}
+
+static auto push_iterator(lua_State* state,
+ const database::Iterator& it) -> void {
+ database::Iterator** data = reinterpret_cast<database::Iterator**>(
+ lua_newuserdata(state, sizeof(uintptr_t)));
+ *data = new database::Iterator(it);
+ luaL_setmetatable(state, kDbIteratorMetatable);
+}
+
+static auto db_iterate_prev(lua_State* state) -> int {
+ database::Iterator* it = db_check_iterator(state, 1);
+ std::optional<database::Record> res = (*it)--;
+
+ if (res) {
+ push_lua_record(state, *res);
+ } else {
+ lua_pushnil(state);
+ }
+
+ return 1;
+}
+
+static auto db_iterate(lua_State* state) -> int {
+ database::Iterator* it = db_check_iterator(state, 1);
+ std::optional<database::Record> res = (*it)++;
+
+ if (res) {
+ push_lua_record(state, *res);
+ } else {
+ lua_pushnil(state);
+ }
+
+ return 1;
+}
+
+static auto db_iterator_clone(lua_State* state) -> int {
+ database::Iterator* it = db_check_iterator(state, 1);
+ push_iterator(state, *it);
+ return 1;
+}
+
+static auto db_iterator_gc(lua_State* state) -> int {
+ database::Iterator* it = db_check_iterator(state, 1);
+ delete it;
+ return 0;
+}
+
+static const struct luaL_Reg kDbIteratorFuncs[] = {
+ {"next", db_iterate}, {"prev", db_iterate_prev},
+ {"clone", db_iterator_clone}, {"__call", db_iterate},
+ {"__gc", db_iterator_gc}, {NULL, NULL}};
+
+static auto record_text(lua_State* state) -> int {
+ LuaRecord* data = reinterpret_cast<LuaRecord*>(
+ luaL_checkudata(state, 1, kDbRecordMetatable));
+ lua_pushlstring(state, data->text, data->text_size);
+ return 1;
+}
+
+static auto record_contents(lua_State* state) -> int {
+ LuaRecord* data = reinterpret_cast<LuaRecord*>(
+ luaL_checkudata(state, 1, kDbRecordMetatable));
+
+ std::visit(
+ [&](auto&& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, database::TrackId>) {
+ lua_pushinteger(state, arg);
+ } else if constexpr (std::is_same_v<T, database::IndexKey::Header>) {
+ Bridge* bridge = Bridge::Get(state);
+ auto db = bridge->services().database().lock();
+ if (!db) {
+ lua_pushnil(state);
+ } else {
+ push_iterator(state, database::Iterator{db, arg});
+ }
+ }
+ },
+ data->contents);
+
+ return 1;
+}
+
+static const struct luaL_Reg kDbRecordFuncs[] = {{"title", record_text},
+ {"contents", record_contents},
+ {"__tostring", record_text},
+ {NULL, NULL}};
+
+static auto index_name(lua_State* state) -> int {
+ LuaIndexInfo* data = reinterpret_cast<LuaIndexInfo*>(
+ luaL_checkudata(state, 1, kDbIndexMetatable));
+ if (data == NULL) {
+ return 0;
+ }
+ lua_pushlstring(state, data->name_data, data->name_size);
+ return 1;
+}
+
+static auto index_iter(lua_State* state) -> int {
+ LuaIndexInfo* data = reinterpret_cast<LuaIndexInfo*>(
+ luaL_checkudata(state, 1, kDbIndexMetatable));
+ if (data == NULL) {
+ return 0;
+ }
+ Bridge* bridge = Bridge::Get(state);
+ auto db = bridge->services().database().lock();
+ if (!db) {
+ lua_pushnil(state);
+ }
+ push_iterator(state, database::Iterator{db, data->id});
+ return 1;
+}
+
+static const struct luaL_Reg kDbIndexFuncs[] = {{"name", index_name},
+ {"iter", index_iter},
+ {"__tostring", index_name},
+ {NULL, NULL}};
+
+static auto lua_database(lua_State* state) -> int {
+ // Metatable for indexes
+ luaL_newmetatable(state, kDbIndexMetatable);
+
+ lua_pushliteral(state, "__index");
+ lua_pushvalue(state, -2);
+ lua_settable(state, -3); // metatable.__index = metatable
+
+ // Add member funcs to the metatable.
+ luaL_setfuncs(state, kDbIndexFuncs, 0);
+
+ luaL_newmetatable(state, kDbIteratorMetatable);
+ lua_pushliteral(state, "__index");
+ lua_pushvalue(state, -2);
+ lua_settable(state, -3); // metatable.__index = metatable
+ luaL_setfuncs(state, kDbIteratorFuncs, 0);
+
+ luaL_newmetatable(state, kDbRecordMetatable);
+ lua_pushliteral(state, "__index");
+ lua_pushvalue(state, -2);
+ lua_settable(state, -3); // metatable.__index = metatable
+ luaL_setfuncs(state, kDbRecordFuncs, 0);
+
+ luaL_newlib(state, kDatabaseFuncs);
+ return 1;
+}
+
+auto RegisterDatabaseModule(lua_State* s) -> void {
+ luaL_requiref(s, "database", lua_database, true);
+ lua_pop(s, 1);
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_database.hpp b/src/tangara/lua/lua_database.hpp
new file mode 100644
index 00000000..328004ef
--- /dev/null
+++ b/src/tangara/lua/lua_database.hpp
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include "lua.hpp"
+
+#include "database/database.hpp"
+
+namespace lua {
+
+auto db_check_iterator(lua_State*, int stack_pos) -> database::Iterator*;
+
+auto RegisterDatabaseModule(lua_State*) -> void;
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_queue.cpp b/src/tangara/lua/lua_queue.cpp
new file mode 100644
index 00000000..bc393aa5
--- /dev/null
+++ b/src/tangara/lua/lua_queue.cpp
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/lua_database.hpp"
+
+#include <memory>
+#include <string>
+
+#include "lua.hpp"
+
+#include "esp_log.h"
+#include "lauxlib.h"
+#include "lua.h"
+#include "lvgl.h"
+
+#include "audio/track_queue.hpp"
+#include "database/database.hpp"
+#include "database/index.hpp"
+#include "database/track.hpp"
+#include "events/event_queue.hpp"
+#include "lua/bridge.hpp"
+#include "lua/property.hpp"
+#include "system_fsm/service_locator.hpp"
+#include "ui/ui_events.hpp"
+
+namespace lua {
+
+[[maybe_unused]] static constexpr char kTag[] = "lua_queue";
+
+static auto queue_add(lua_State* state) -> int {
+ Bridge* instance = Bridge::Get(state);
+
+ if (lua_isinteger(state, 1)) {
+ database::TrackId id = luaL_checkinteger(state, 1);
+ instance->services().bg_worker().Dispatch<void>([=]() {
+ audio::TrackQueue& queue = instance->services().track_queue();
+ queue.append(id);
+ });
+ } else {
+ database::Iterator* it = db_check_iterator(state, 1);
+ instance->services().bg_worker().Dispatch<void>([=]() {
+ audio::TrackQueue& queue = instance->services().track_queue();
+ queue.append(database::TrackIterator{*it});
+ });
+ }
+
+ return 0;
+}
+
+static auto queue_clear(lua_State* state) -> int {
+ Bridge* instance = Bridge::Get(state);
+ audio::TrackQueue& queue = instance->services().track_queue();
+ queue.clear();
+ return 0;
+}
+
+static const struct luaL_Reg kQueueFuncs[] = {{"add", queue_add},
+ {"clear", queue_clear},
+ {NULL, NULL}};
+
+static auto lua_queue(lua_State* state) -> int {
+ luaL_newlib(state, kQueueFuncs);
+ return 1;
+}
+
+auto RegisterQueueModule(lua_State* s) -> void {
+ luaL_requiref(s, "queue", lua_queue, true);
+ lua_pop(s, 1);
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_queue.hpp b/src/tangara/lua/lua_queue.hpp
new file mode 100644
index 00000000..c5cfe04d
--- /dev/null
+++ b/src/tangara/lua/lua_queue.hpp
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include "lua.hpp"
+
+namespace lua {
+
+auto RegisterQueueModule(lua_State*) -> void;
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_registry.hpp b/src/tangara/lua/lua_registry.hpp
new file mode 100644
index 00000000..e556b6eb
--- /dev/null
+++ b/src/tangara/lua/lua_registry.hpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "lua.hpp"
+
+#include "lua/bridge.hpp"
+#include "lua/lua_thread.hpp"
+#include "system_fsm/service_locator.hpp"
+
+namespace lua {
+
+class Registry {
+ public:
+ static auto instance(system_fsm::ServiceLocator&) -> Registry&;
+
+ auto uiThread() -> std::shared_ptr<LuaThread>;
+ auto newThread() -> std::shared_ptr<LuaThread>;
+
+ auto AddPropertyModule(
+ const std::string&,
+ std::vector<std::pair<std::string, std::variant<LuaFunction, Property*>>>)
+ -> void;
+
+ Registry(const Registry&) = delete;
+ Registry& operator=(const Registry&) = delete;
+
+ private:
+ Registry(system_fsm::ServiceLocator&);
+
+ system_fsm::ServiceLocator& services_;
+ std::unique_ptr<Bridge> bridge_;
+
+ std::shared_ptr<LuaThread> ui_thread_;
+ std::list<std::weak_ptr<LuaThread>> threads_;
+
+ std::vector<
+ std::pair<std::string,
+ std::vector<std::pair<std::string,
+ std::variant<LuaFunction, Property*>>>>>
+ modules_;
+};
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_screen.cpp b/src/tangara/lua/lua_screen.cpp
new file mode 100644
index 00000000..8d87eebd
--- /dev/null
+++ b/src/tangara/lua/lua_screen.cpp
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/lua_screen.hpp"
+
+#include <memory>
+#include <string>
+
+#include "lua.hpp"
+
+#include "esp_log.h"
+#include "lauxlib.h"
+#include "lua.h"
+#include "lvgl.h"
+
+#include "audio/track_queue.hpp"
+#include "database/database.hpp"
+#include "database/index.hpp"
+#include "database/track.hpp"
+#include "events/event_queue.hpp"
+#include "lua/bridge.hpp"
+#include "lua/property.hpp"
+#include "system_fsm/service_locator.hpp"
+#include "ui/ui_events.hpp"
+
+namespace lua {
+
+static auto screen_new(lua_State* L) -> int {
+ // o = o or {}
+ if (lua_gettop(L) != 2) {
+ lua_settop(L, 1);
+ lua_newtable(L);
+ }
+ // Swap o and self on the stack.
+ lua_insert(L, 1);
+
+ lua_pushliteral(L, "__index");
+ lua_pushvalue(L, 1);
+ lua_settable(L, 1); // self.__index = self
+
+ lua_setmetatable(L, 1); // setmetatable(o, self)
+
+ return 1; // return o
+}
+
+static auto screen_noop(lua_State* state) -> int {
+ return 0;
+}
+
+static auto screen_true(lua_State* state) -> int {
+ lua_pushboolean(state, true);
+ return 1;
+}
+
+static const struct luaL_Reg kScreenFuncs[] = {
+ {"new", screen_new}, {"createUi", screen_noop},
+ {"onShown", screen_noop}, {"onHidden", screen_noop},
+ {"canPop", screen_true}, {NULL, NULL}};
+
+static auto lua_screen(lua_State* state) -> int {
+ luaL_newlib(state, kScreenFuncs);
+
+ lua_pushliteral(state, "__index");
+ lua_pushvalue(state, -2);
+ lua_rawset(state, -3);
+
+ return 1;
+}
+
+auto RegisterScreenModule(lua_State* s) -> void {
+ luaL_requiref(s, "screen", lua_screen, true);
+
+ lua_pop(s, 1);
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_screen.hpp b/src/tangara/lua/lua_screen.hpp
new file mode 100644
index 00000000..1c3bed1a
--- /dev/null
+++ b/src/tangara/lua/lua_screen.hpp
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include "lua.hpp"
+
+namespace lua {
+
+auto RegisterScreenModule(lua_State*) -> void;
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_theme.cpp b/src/tangara/lua/lua_theme.cpp
new file mode 100644
index 00000000..5edde104
--- /dev/null
+++ b/src/tangara/lua/lua_theme.cpp
@@ -0,0 +1,92 @@
+
+/*
+ * Copyright 2023 ailurux <ailuruxx@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/lua_version.hpp"
+
+#include <string>
+
+#include "lua.hpp"
+#include "lua/bridge.hpp"
+
+#include "esp_app_desc.h"
+#include "esp_log.h"
+#include "lauxlib.h"
+#include "lua.h"
+#include "lua/lua_thread.hpp"
+#include "luavgl.h"
+#include "ui/themes.hpp"
+
+namespace lua {
+
+static auto set_style(lua_State* L) -> int {
+ // Get the object and class name from the stack
+ std::string class_name = luaL_checkstring(L, -1);
+ lv_obj_t* obj = luavgl_to_obj(L, -2);
+ if (obj != NULL) {
+ ui::themes::Theme::instance()->ApplyStyle(obj, class_name);
+ }
+ return 0;
+}
+
+static auto set_theme(lua_State* L) -> int {
+ std::string class_name;
+ luaL_checktype(L, -1, LUA_TTABLE);
+ lua_pushnil(L); /* first key */
+ while (lua_next(L, -2) != 0) {
+ /* uses 'key' (at index -2) and 'value' (at index -1) */
+ if (lua_type(L, -2) == LUA_TSTRING) {
+ class_name = lua_tostring(L, -2);
+ }
+ if (lua_type(L, -1) == LUA_TTABLE) {
+ // Nesting
+ lua_pushnil(L); // First key
+ while (lua_next(L, -2) != 0) {
+ // Nesting the second
+ int selector = -1;
+ lua_pushnil(L); // First key
+ while (lua_next(L, -2) != 0) {
+ int idx = lua_tointeger(L, -2);
+ if (idx == 1) {
+ // Selector
+ selector = lua_tointeger(L, -1);
+ } else if (idx == 2) {
+ // Style
+ lv_style_t* style = luavgl_to_style(L, -1);
+ if (style == NULL) {
+ ESP_LOGI("lua_theme", "Style was null or malformed");
+ return 0;
+ } else {
+ ui::themes::Theme::instance()->AddStyle(class_name, selector,
+ style);
+ }
+ }
+ lua_pop(L, 1);
+ }
+ lua_pop(L, 1);
+ }
+ }
+ /* removes 'value'; keeps 'key' for next iteration */
+ lua_pop(L, 1);
+ }
+ return 0;
+}
+
+static const struct luaL_Reg kThemeFuncs[] = {{"set", set_theme},
+ {"set_style", set_style},
+ {NULL, NULL}};
+
+static auto lua_theme(lua_State* L) -> int {
+ luaL_newlib(L, kThemeFuncs);
+ return 1;
+}
+
+auto RegisterThemeModule(lua_State* L) -> void {
+ luaL_requiref(L, "theme", lua_theme, true);
+ lua_pop(L, 1);
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_theme.hpp b/src/tangara/lua/lua_theme.hpp
new file mode 100644
index 00000000..fed710e0
--- /dev/null
+++ b/src/tangara/lua/lua_theme.hpp
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2024 ailurux <ailuruxx@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include "lua.hpp"
+
+namespace lua {
+
+auto RegisterThemeModule(lua_State*) -> void;
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_thread.cpp b/src/tangara/lua/lua_thread.cpp
new file mode 100644
index 00000000..77a46b45
--- /dev/null
+++ b/src/tangara/lua/lua_thread.cpp
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/lua_thread.hpp"
+
+#include <iostream>
+#include <memory>
+
+#include "esp_heap_caps.h"
+#include "esp_log.h"
+#include "lua.hpp"
+
+#include "events/event_queue.hpp"
+#include "lua/bridge.hpp"
+#include "memory_resource.hpp"
+#include "system_fsm/service_locator.hpp"
+#include "ui/ui_events.hpp"
+
+namespace lua {
+
+[[maybe_unused]] static constexpr char kTag[] = "lua";
+
+class Allocator {
+ public:
+ Allocator() : total_allocated_(0) {}
+
+ auto alloc(void* ptr, size_t osize, size_t nsize) -> void* {
+ total_allocated_ = total_allocated_ - osize + nsize;
+ // ESP_LOGI(kTag, "lua realloc -> %u KiB", total_allocated_ / 1024);
+ if (nsize == 0) {
+ heap_caps_free(ptr);
+ return NULL;
+ } else {
+ return heap_caps_realloc(ptr, nsize, MALLOC_CAP_SPIRAM);
+ }
+ }
+
+ private:
+ size_t total_allocated_;
+};
+
+static auto lua_alloc(void* ud,
+ void* ptr,
+ size_t osize,
+ size_t nsize) -> void* {
+ Allocator* instance = reinterpret_cast<Allocator*>(ud);
+ return instance->alloc(ptr, osize, nsize);
+}
+
+static int lua_panic(lua_State* L) {
+ ESP_LOGE(kTag, "!! PANIC !! %s", lua_tostring(L, -1));
+ return 0;
+}
+
+auto LuaThread::Start(system_fsm::ServiceLocator& services) -> LuaThread* {
+ auto alloc = std::make_unique<Allocator>();
+ lua_State* state = lua_newstate(lua_alloc, alloc.get());
+ if (!state) {
+ return nullptr;
+ }
+
+ luaL_openlibs(state);
+ lua_atpanic(state, lua_panic);
+
+ return new LuaThread(alloc, state);
+}
+
+LuaThread::LuaThread(std::unique_ptr<Allocator>& alloc, lua_State* state)
+ : alloc_(std::move(alloc)), state_(state) {}
+
+LuaThread::~LuaThread() {
+ lua_close(state_);
+}
+
+auto LuaThread::RunScript(const std::string& path) -> bool {
+ int res = luaL_loadfilex(state_, path.c_str(), NULL);
+ if (res != LUA_OK) {
+ return false;
+ }
+ CallProtected(state_, 0, 0);
+ return true;
+}
+
+auto LuaThread::RunString(const std::string& script) -> bool {
+ int res = luaL_loadstring(state_, script.c_str());
+ if (res != LUA_OK) {
+ return false;
+ }
+ CallProtected(state_, 0, 0);
+ return true;
+}
+
+auto LuaThread::DumpStack() -> void {
+ int top = lua_gettop(state_);
+ std::cout << "stack size: " << top << std::endl;
+ for (size_t i = 1; i <= top; i++) {
+ std::cout << "[" << i << "]\t" << luaL_typename(state_, i);
+ switch (lua_type(state_, i)) {
+ case LUA_TNUMBER:
+ std::cout << "\t(";
+ if (lua_isinteger(state_, i)) {
+ std::cout << lua_tointeger(state_, i);
+ } else {
+ std::cout << lua_tonumber(state_, i);
+ }
+ std::cout << ")";
+ break;
+ case LUA_TSTRING:
+ std::cout << "\t('" << lua_tostring(state_, i) << "')";
+ break;
+ case LUA_TBOOLEAN:
+ std::cout << "\t(" << lua_toboolean(state_, i) << ")";
+ break;
+ case LUA_TNIL:
+ // Value is implied.
+ break;
+ case LUA_TTABLE:
+ lua_pushnil(state_);
+ while (lua_next(state_, i) != 0) {
+ // Keys
+ std::cout << std::endl << "\t\t" << luaL_typename(state_, -2);
+ if (lua_type(state_, -2) == LUA_TSTRING) {
+ std::cout << "\t(" << lua_tostring(state_, -2) << ")";
+ } else if (lua_type(state_, -2) == LUA_TNUMBER) {
+ std::cout << "\t(" << lua_tonumber(state_, -2) << ")";
+ }
+
+ // Values
+ std::cout << "\t\t" << luaL_typename(state_, -1);
+ if (lua_type(state_, -1) == LUA_TSTRING) {
+ std::cout << "\t(" << lua_tostring(state_, -1) << ")";
+ } else if (lua_type(state_, -1) == LUA_TNUMBER) {
+ std::cout << "\t(" << lua_tonumber(state_, -1) << ")";
+ }
+ // Pop the value; we don't care about it. Leave the key on the stack
+ // for the next call to lua_next.
+ lua_pop(state_, 1);
+ }
+ break;
+ default:
+ std::cout << "\t(" << lua_topointer(state_, i) << ")";
+ break;
+ }
+ std::cout << std::endl;
+ }
+}
+
+static int msg_handler(lua_State* L) {
+ if (!lua_isstring(L, 1)) {
+ return 1;
+ }
+
+ const char* msg = lua_tostring(L, 1);
+ if (msg == NULL) { /* is error object not a string? */
+ if (luaL_callmeta(L, 1, "__tostring") && /* does it have a metamethod */
+ lua_type(L, -1) == LUA_TSTRING) /* that produces a string? */
+ return 1; /* that is the message */
+ else
+ msg = lua_pushfstring(L, "(error object is a %s value)",
+ luaL_typename(L, 1));
+ }
+
+ /* append a standard traceback */
+ luaL_traceback(L, L, msg, 1);
+ return 1;
+}
+
+auto CallProtected(lua_State* s, int nargs, int nresults) -> int {
+ int base = lua_gettop(s) - nargs;
+ // Place our message handler under the function to be called.
+ lua_pushcfunction(s, msg_handler);
+ lua_insert(s, base);
+
+ // Invoke the function.
+ int ret = lua_pcall(s, nargs, nresults, base);
+ if (ret != LUA_OK) {
+ events::Ui().Dispatch(ui::OnLuaError{.message = lua_tostring(s, -1)});
+ }
+
+ // Clean up our message handler
+ lua_remove(s, base);
+
+ return ret;
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_thread.hpp b/src/tangara/lua/lua_thread.hpp
new file mode 100644
index 00000000..d7602c1e
--- /dev/null
+++ b/src/tangara/lua/lua_thread.hpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "lua.hpp"
+
+#include "system_fsm/service_locator.hpp"
+
+namespace lua {
+
+class Allocator;
+
+auto CallProtected(lua_State*, int nargs, int nresults) -> int;
+
+class LuaThread {
+ public:
+ static auto Start(system_fsm::ServiceLocator&) -> LuaThread*;
+ ~LuaThread();
+
+ auto RunScript(const std::string& path) -> bool;
+ auto RunString(const std::string& path) -> bool;
+
+ auto DumpStack() -> void;
+
+ auto state() -> lua_State* { return state_; }
+
+ LuaThread(const LuaThread&) = delete;
+ LuaThread& operator=(const LuaThread&) = delete;
+
+ private:
+ LuaThread(std::unique_ptr<Allocator>&, lua_State*);
+
+ std::unique_ptr<Allocator> alloc_;
+ lua_State* state_;
+};
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_version.cpp b/src/tangara/lua/lua_version.cpp
new file mode 100644
index 00000000..b85a30a5
--- /dev/null
+++ b/src/tangara/lua/lua_version.cpp
@@ -0,0 +1,68 @@
+
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/lua_version.hpp"
+
+#include <string>
+
+#include "lua.hpp"
+#include "lua/bridge.hpp"
+
+#include "esp_app_desc.h"
+#include "esp_log.h"
+#include "lauxlib.h"
+#include "lua.h"
+#include "lua/lua_thread.hpp"
+
+namespace lua {
+
+static auto esp(lua_State* L) -> int {
+ auto desc = esp_app_get_description();
+ lua_pushstring(L, desc->version);
+ return 1;
+}
+
+static auto samd(lua_State* L) -> int {
+ Bridge* instance = Bridge::Get(L);
+ auto& samd = instance->services().samd();
+ auto version = samd.Version();
+ lua_pushlstring(L, version.data(), version.size());
+ return 1;
+}
+
+static auto update_samd(lua_State* L) -> int {
+ Bridge* instance = Bridge::Get(L);
+ auto& samd = instance->services().samd();
+ samd.ResetToFlashSamd();
+ return 0;
+}
+
+static auto collator(lua_State* L) -> int {
+ Bridge* instance = Bridge::Get(L);
+ auto& collator = instance->services().collator();
+ auto version = collator.Describe().value_or("None");
+ lua_pushlstring(L, version.data(), version.size());
+ return 1;
+}
+
+static const struct luaL_Reg kVersionFuncs[] = {{"esp", esp},
+ {"samd", samd},
+ {"collator", collator},
+ {"update_samd", update_samd},
+ {NULL, NULL}};
+
+static auto lua_version(lua_State* L) -> int {
+ luaL_newlib(L, kVersionFuncs);
+ return 1;
+}
+
+auto RegisterVersionModule(lua_State* L) -> void {
+ luaL_requiref(L, "version", lua_version, true);
+ lua_pop(L, 1);
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/lua_version.hpp b/src/tangara/lua/lua_version.hpp
new file mode 100644
index 00000000..4ba4be94
--- /dev/null
+++ b/src/tangara/lua/lua_version.hpp
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include "lua.hpp"
+
+namespace lua {
+
+auto RegisterVersionModule(lua_State*) -> void;
+
+} // namespace lua
diff --git a/src/tangara/lua/property.cpp b/src/tangara/lua/property.cpp
new file mode 100644
index 00000000..2b93809d
--- /dev/null
+++ b/src/tangara/lua/property.cpp
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/property.hpp"
+#include <sys/_stdint.h>
+
+#include <cmath>
+#include <memory>
+#include <memory_resource>
+#include <sstream>
+#include <string>
+#include <variant>
+
+#include "database/track.hpp"
+#include "drivers/bluetooth_types.hpp"
+#include "lauxlib.h"
+#include "lua.h"
+#include "lua.hpp"
+#include "lua/lua_thread.hpp"
+#include "lvgl.h"
+#include "memory_resource.hpp"
+#include "system_fsm/service_locator.hpp"
+#include "types.hpp"
+
+namespace lua {
+
+static const char kPropertyMetatable[] = "property";
+static const char kFunctionMetatable[] = "c_func";
+static const char kBindingMetatable[] = "binding";
+static const char kBindingsTable[] = "bindings";
+static const char kBinderKey[] = "binder";
+
+auto Binding::get(lua_State* L, int idx) -> Binding* {
+ return reinterpret_cast<Binding*>(luaL_testudata(L, idx, kBindingMetatable));
+}
+
+auto Binding::apply(lua_State* L, int idx) -> bool {
+ Binding* b = get(L, idx);
+ if (b->dirty && b->active) {
+ b->dirty = false;
+ // The binding needs to be reapplied. Push the Lua callback, then its arg.
+ lua_getiuservalue(L, idx, 1);
+ b->property->pushValue(*L);
+
+ // Invoke the callback.
+ return CallProtected(L, 1, 0) == LUA_OK;
+ }
+ return true;
+}
+
+static auto check_property(lua_State* state) -> Property* {
+ void* data = luaL_checkudata(state, 1, kPropertyMetatable);
+ luaL_argcheck(state, data != NULL, 1, "`property` expected");
+ return *reinterpret_cast<Property**>(data);
+}
+
+static auto property_get(lua_State* state) -> int {
+ Property* p = check_property(state);
+ p->pushValue(*state);
+ return 1;
+}
+
+static auto property_set(lua_State* state) -> int {
+ Property* p = check_property(state);
+ luaL_argcheck(state, p->isTwoWay(), 1, "property is read-only");
+ bool valid = p->popValue(*state);
+ lua_pushboolean(state, valid);
+ return 1;
+}
+
+static auto property_bind(lua_State* state) -> int {
+ Property* p = check_property(state);
+ luaL_checktype(state, 2, LUA_TFUNCTION);
+
+ // Fetch the table of live bindings.
+ lua_pushstring(state, kBindingsTable);
+ lua_gettable(state, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable]
+
+ // Create the userdata holding the new binding's metadata.
+ Binding* binding =
+ reinterpret_cast<Binding*>(lua_newuserdatauv(state, sizeof(Binding), 1));
+ *binding = Binding{.property = p, .active = true, .dirty = true};
+ luaL_setmetatable(state, kBindingMetatable);
+
+ // Associate the callback function with the new binding.
+ lua_pushvalue(state, 2);
+ lua_setiuservalue(state, -2, 1);
+
+ // Put a reference to the binding into the bindings table, so that we can
+ // look it up later.
+ lua_pushvalue(state, -1);
+ int binding_ref = luaL_ref(state, 3);
+
+ // Tell the property about the new binding. This was also perform the initial
+ // bind.
+ p->addLuaBinding(state, binding_ref);
+
+ // Return the only remaining strong reference to the new Binding.
+ return 1;
+}
+
+static auto property_tostring(lua_State* state) -> int {
+ Property* p = check_property(state);
+ p->pushValue(*state);
+
+ std::stringstream str{};
+ str << "property { " << luaL_tolstring(state, -1, NULL);
+ if (!p->isTwoWay()) {
+ str << ", read-only";
+ }
+ str << " }";
+
+ lua_settop(state, 0);
+
+ std::string res = str.str();
+ lua_pushlstring(state, res.data(), res.size());
+ return 1;
+}
+
+static const struct luaL_Reg kPropertyBindingFuncs[] = {
+ {"get", property_get},
+ {"set", property_set},
+ {"bind", property_bind},
+ {"__tostring", property_tostring},
+ {NULL, NULL}};
+
+static auto generic_function_cb(lua_State* state) -> int {
+ lua_pushstring(state, kBinderKey);
+ lua_gettable(state, LUA_REGISTRYINDEX);
+ PropertyBindings* binder =
+ reinterpret_cast<PropertyBindings*>(lua_touserdata(state, -1));
+
+ size_t* index =
+ reinterpret_cast<size_t*>(luaL_checkudata(state, 1, kFunctionMetatable));
+ const LuaFunction& fn = binder->GetFunction(*index);
+
+ // Ensure the C++ function is called with a clean stack; we don't want it to
+ // see the index we just used.
+ lua_remove(state, 1);
+
+ return std::invoke(fn, state);
+}
+
+PropertyBindings::PropertyBindings() : functions_(&memory::kSpiRamResource) {}
+
+auto PropertyBindings::install(lua_State* L) -> void {
+ lua_pushstring(L, kBinderKey);
+ lua_pushlightuserdata(L, this);
+ lua_settable(L, LUA_REGISTRYINDEX);
+
+ // Create the metatable responsible for the Property API.
+ luaL_newmetatable(L, kPropertyMetatable);
+
+ lua_pushliteral(L, "__index");
+ lua_pushvalue(L, -2);
+ lua_settable(L, -3); // metatable.__index = metatable
+
+ // Add our binding funcs (get, set, bind) to the metatable.
+ luaL_setfuncs(L, kPropertyBindingFuncs, 0);
+
+ // We've finished setting up the metatable, so pop it.
+ lua_pop(L, 1);
+
+ // Create the metatable responsible for each Binding. This metatable is empty
+ // as it's only used for identification.
+ luaL_newmetatable(L, kBindingMetatable);
+ lua_pop(L, 1);
+
+ // Create a weak table in the registry to hold live bindings.
+ lua_pushstring(L, kBindingsTable);
+ lua_newtable(L); // bindings = {}
+
+ // Metatable for the weak table. Values are weak.
+ lua_newtable(L); // meta = {}
+ lua_pushliteral(L, "__mode");
+ lua_pushliteral(L, "v");
+ lua_settable(L, -3); // meta.__mode='v'
+ lua_setmetatable(L, -2); // setmetatable(bindings, meta)
+
+ lua_settable(L, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings
+
+ // Create the metatable for C++ functions.
+ luaL_newmetatable(L, kFunctionMetatable);
+
+ lua_pushliteral(L, "__call");
+ lua_pushcfunction(L, generic_function_cb);
+ lua_settable(L, -3); // metatable.__call = metatable
+
+ lua_pop(L, 1); // Clean up the function metatable
+}
+
+auto PropertyBindings::Register(lua_State* s, Property* prop) -> void {
+ Property** data =
+ reinterpret_cast<Property**>(lua_newuserdata(s, sizeof(uintptr_t)));
+ *data = prop;
+
+ luaL_setmetatable(s, kPropertyMetatable);
+}
+
+auto PropertyBindings::Register(lua_State* s, LuaFunction fn) -> void {
+ size_t* index = reinterpret_cast<size_t*>(lua_newuserdata(s, sizeof(size_t)));
+ *index = functions_.size();
+ functions_.push_back(fn);
+
+ luaL_setmetatable(s, kFunctionMetatable);
+}
+
+auto PropertyBindings::GetFunction(size_t i) -> const LuaFunction& {
+ assert(i < functions_.size());
+ return functions_[i];
+};
+
+template <class... Ts>
+inline constexpr bool always_false_v = false;
+
+Property::Property(const LuaValue& val)
+ : value_(memory::SpiRamAllocator<LuaValue>().new_object<LuaValue>(val)),
+ cb_(),
+ bindings_(&memory::kSpiRamResource) {}
+
+Property::Property(const LuaValue& val,
+ std::function<bool(const LuaValue& val)> cb)
+ : value_(memory::SpiRamAllocator<LuaValue>().new_object<LuaValue>(val)),
+ cb_(cb),
+ bindings_(&memory::kSpiRamResource) {}
+
+auto Property::setDirect(const LuaValue& val) -> void {
+ *value_ = val;
+ reapplyAll();
+}
+
+auto Property::set(const LuaValue& val) -> bool {
+ if (cb_ && !std::invoke(*cb_, val)) {
+ return false;
+ }
+ setDirect(val);
+ return true;
+}
+
+static auto pushTagValue(lua_State* L, const database::TagValue& val) -> void {
+ std::visit(
+ [&](auto&& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, std::pmr::string>) {
+ lua_pushlstring(L, arg.data(), arg.size());
+ } else if constexpr (std::is_same_v<
+ T, std::span<const std::pmr::string>>) {
+ lua_createtable(L, 0, arg.size());
+ for (const auto& i : arg) {
+ lua_pushlstring(L, i.data(), i.size());
+ lua_pushboolean(L, true);
+ lua_rawset(L, -3);
+ }
+ } else if constexpr (std::is_same_v<T, uint32_t>) {
+ lua_pushinteger(L, arg);
+ } else {
+ lua_pushnil(L);
+ }
+ },
+ val);
+}
+
+static void pushTrack(lua_State* L, const audio::TrackInfo& track) {
+ lua_newtable(L);
+
+ for (const auto& tag : track.tags->allPresent()) {
+ lua_pushstring(L, database::tagName(tag).c_str());
+ pushTagValue(L, track.tags->get(tag));
+ lua_settable(L, -3);
+ }
+
+ if (track.duration) {
+ lua_pushliteral(L, "duration");
+ lua_pushinteger(L, track.duration.value());
+ lua_settable(L, -3);
+ }
+
+ if (track.bitrate_kbps) {
+ lua_pushliteral(L, "bitrate_kbps");
+ lua_pushinteger(L, track.bitrate_kbps.value());
+ lua_settable(L, -3);
+ }
+
+ lua_pushliteral(L, "encoding");
+ lua_pushstring(L, codecs::StreamTypeToString(track.encoding).c_str());
+ lua_settable(L, -3);
+}
+
+static void pushDevice(lua_State* L, const drivers::bluetooth::Device& dev) {
+ lua_createtable(L, 0, 4);
+
+ lua_pushliteral(L, "address");
+ auto* mac = reinterpret_cast<drivers::bluetooth::mac_addr_t*>(
+ lua_newuserdata(L, sizeof(drivers::bluetooth::mac_addr_t)));
+ *mac = dev.address;
+ lua_rawset(L, -3);
+
+ // What I just did there was perfectly safe. Look, I can prove it:
+ static_assert(
+ std::is_trivially_copy_assignable<drivers::bluetooth::mac_addr_t>());
+ static_assert(
+ std::is_trivially_destructible<drivers::bluetooth::mac_addr_t>());
+
+ lua_pushliteral(L, "name");
+ lua_pushlstring(L, dev.name.data(), dev.name.size());
+ lua_rawset(L, -3);
+
+ // FIXME: This field deserves a little more structure.
+ lua_pushliteral(L, "class");
+ lua_pushinteger(L, dev.class_of_device);
+ lua_rawset(L, -3);
+
+ lua_pushliteral(L, "signal_strength");
+ lua_pushinteger(L, dev.signal_strength);
+ lua_rawset(L, -3);
+}
+
+auto Property::pushValue(lua_State& s) -> int {
+ std::visit(
+ [&](auto&& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_same_v<T, std::monostate>) {
+ lua_pushnil(&s);
+ } else if constexpr (std::is_same_v<T, int>) {
+ lua_pushinteger(&s, arg);
+ } else if constexpr (std::is_same_v<T, bool>) {
+ lua_pushboolean(&s, arg);
+ } else if constexpr (std::is_same_v<T, std::string>) {
+ lua_pushstring(&s, arg.c_str());
+ } else if constexpr (std::is_same_v<T, audio::TrackInfo>) {
+ pushTrack(&s, arg);
+ } else if constexpr (std::is_same_v<T, drivers::bluetooth::Device>) {
+ pushDevice(&s, arg);
+ } else if constexpr (std::is_same_v<
+ T, std::vector<drivers::bluetooth::Device>>) {
+ lua_createtable(&s, arg.size(), 0);
+ size_t i = 1;
+ for (const auto& dev : arg) {
+ pushDevice(&s, dev);
+ lua_rawseti(&s, -2, i++);
+ }
+ } else {
+ static_assert(always_false_v<T>, "PushValue missing type");
+ }
+ },
+ *value_);
+ return 1;
+}
+
+auto popRichType(lua_State* L) -> LuaValue {
+ lua_pushliteral(L, "address");
+ lua_gettable(L, -2);
+
+ if (lua_isuserdata(L, -1)) {
+ // This must be a bt device!
+ drivers::bluetooth::mac_addr_t mac =
+ *reinterpret_cast<drivers::bluetooth::mac_addr_t*>(
+ lua_touserdata(L, -1));
+ lua_pop(L, 1);
+
+ lua_pushliteral(L, "name");
+ lua_gettable(L, -2);
+
+ std::pmr::string name = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ return drivers::bluetooth::Device{
+ .address = mac,
+ .name = name,
+ .class_of_device = 0,
+ .signal_strength = 0,
+ };
+ }
+
+ return std::monostate{};
+}
+
+auto Property::popValue(lua_State& s) -> bool {
+ LuaValue new_val;
+ switch (lua_type(&s, 2)) {
+ case LUA_TNIL:
+ new_val = std::monostate{};
+ break;
+ case LUA_TNUMBER:
+ if (lua_isinteger(&s, 2)) {
+ new_val = lua_tointeger(&s, 2);
+ } else {
+ new_val = static_cast<lua_Integer>(std::round(lua_tonumber(&s, 2)));
+ }
+ break;
+ case LUA_TBOOLEAN:
+ new_val = static_cast<bool>(lua_toboolean(&s, 2));
+ break;
+ case LUA_TSTRING:
+ new_val = lua_tostring(&s, 2);
+ break;
+ default:
+ if (lua_istable(&s, 2)) {
+ new_val = popRichType(&s);
+ if (std::holds_alternative<std::monostate>(new_val)) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return set(new_val);
+}
+
+auto Property::reapplyAll() -> void {
+ for (int i = bindings_.size() - 1; i >= 0; i--) {
+ auto& b = bindings_[i];
+ if (!applySingle(b.first, b.second, true)) {
+ // Remove the binding if we weren't able to apply it. This is usually due
+ // to the binding getting GC'd.
+ bindings_.erase(bindings_.begin() + i);
+ }
+ }
+}
+
+auto Property::applySingle(lua_State* L, int ref, bool mark_dirty) -> bool {
+ int top = lua_gettop(L);
+
+ // Push the table of bindings.
+ lua_pushstring(L, kBindingsTable);
+ lua_gettable(L, LUA_REGISTRYINDEX);
+
+ // Resolve the reference.
+ int type = lua_rawgeti(L, -1, ref);
+ if (type == LUA_TNIL) {
+ lua_settop(L, top);
+ return false;
+ }
+
+ // Defensively check that the ref was actually for a Binding.
+ Binding* b = Binding::get(L, -1);
+ if (!b) {
+ lua_settop(L, top);
+ return false;
+ }
+
+ if (mark_dirty) {
+ b->dirty = true;
+ }
+
+ bool ret = Binding::apply(L, -1);
+ lua_settop(L, top);
+ return ret;
+}
+
+auto Property::addLuaBinding(lua_State* state, int ref) -> void {
+ bindings_.push_back({state, ref});
+ applySingle(state, ref, true);
+}
+
+} // namespace lua
diff --git a/src/tangara/lua/property.hpp b/src/tangara/lua/property.hpp
new file mode 100644
index 00000000..9f925766
--- /dev/null
+++ b/src/tangara/lua/property.hpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <memory>
+#include <string>
+
+#include "audio/audio_events.hpp"
+#include "drivers/bluetooth_types.hpp"
+#include "lua.hpp"
+#include "lvgl.h"
+#include "system_fsm/service_locator.hpp"
+
+namespace lua {
+
+// FIXME: We should use some kind of interface for this instead.
+using LuaValue = std::variant<std::monostate,
+ int,
+ bool,
+ std::string,
+ audio::TrackInfo,
+ drivers::bluetooth::Device,
+ std::vector<drivers::bluetooth::Device>>;
+
+using LuaFunction = std::function<int(lua_State*)>;
+
+class Property {
+ public:
+ Property() : Property(std::monostate{}) {}
+ Property(const LuaValue&);
+ Property(const LuaValue&, std::function<bool(const LuaValue&)> filter);
+
+ auto get() -> const LuaValue& { return *value_; }
+
+ /*
+ * Assigns a new value to this property, bypassing the filter fn. All
+ * bindings will be marked as dirty, and if active, will be reapplied.
+ */
+ auto setDirect(const LuaValue&) -> void;
+ /*
+ * Invokes the filter fn, and if successful, assigns the new value to this
+ * property. All bindings will be marked as dirty, and if active, will be
+ * reapplied.
+ */
+ auto set(const LuaValue&) -> bool;
+
+ /* Returns whether or not this Property can be written from Lua. */
+ auto isTwoWay() -> bool { return cb_.has_value(); }
+
+ auto pushValue(lua_State& s) -> int;
+ auto popValue(lua_State& s) -> bool;
+
+ /* Reapplies all active, dirty bindings associated with this Property. */
+ auto reapplyAll() -> void;
+
+ auto addLuaBinding(lua_State*, int ref) -> void;
+ auto applySingle(lua_State*, int ref, bool mark_dirty) -> bool;
+
+ private:
+ std::unique_ptr<LuaValue> value_;
+ std::optional<std::function<bool(const LuaValue&)>> cb_;
+ std::pmr::vector<std::pair<lua_State*, int>> bindings_;
+};
+
+/*
+ * Container for a Lua function that should be invoked whenever a Property's
+ * value changes, as well as some extra accounting metadata.
+ */
+struct Binding {
+ /* Checks the value at idx is a Binding, returning a pointer to it if so. */
+ static auto get(lua_State*, int idx) -> Binding*;
+ /*
+ * If the value at idx is a dirty, active Binding, applies the current value
+ * from its Property. Returns false if the binding was active and dirty, but
+ * invoking the Lua callback failed.
+ */
+ static auto apply(lua_State*, int idx) -> bool;
+
+ Property* property;
+ bool active;
+ bool dirty;
+};
+
+static_assert(std::is_trivially_destructible<Binding>());
+static_assert(std::is_trivially_copy_assignable<Binding>());
+
+class PropertyBindings {
+ public:
+ PropertyBindings();
+
+ auto install(lua_State*) -> void;
+
+ auto Register(lua_State*, Property*) -> void;
+ auto Register(lua_State*, LuaFunction) -> void;
+
+ auto GetFunction(size_t i) -> const LuaFunction&;
+
+ private:
+ std::pmr::vector<LuaFunction> functions_;
+};
+
+} // namespace lua
diff --git a/src/tangara/lua/registry.cpp b/src/tangara/lua/registry.cpp
new file mode 100644
index 00000000..d33594a3
--- /dev/null
+++ b/src/tangara/lua/registry.cpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 jacqueline <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#include "lua/lua_registry.hpp"
+
+#include <iostream>
+#include <memory>
+
+#include "esp_heap_caps.h"
+#include "esp_log.h"
+#include "lua.hpp"
+
+#include "events/event_queue.hpp"
+#include "lua/bridge.hpp"
+#include "memory_resource.hpp"
+#include "system_fsm/service_locator.hpp"
+#include "ui/ui_events.hpp"
+
+namespace lua {
+
+[[maybe_unused]] static constexpr char kTag[] = "lua";
+
+auto Registry::instance(system_fsm::ServiceLocator& s) -> Registry& {
+ static Registry sRegistry{s};
+ return sRegistry;
+}
+
+Registry::Registry(system_fsm::ServiceLocator& services)
+ : services_(services), bridge_(new Bridge(services)) {}
+
+auto Registry::uiThread() -> std::shared_ptr<LuaThread> {
+ if (!ui_thread_) {
+ ui_thread_ = newThread();
+ bridge_->installLvgl(ui_thread_->state());
+ }
+ return ui_thread_;
+}
+
+auto Registry::newThread() -> std::shared_ptr<LuaThread> {
+ std::shared_ptr<LuaThread> thread{LuaThread::Start(services_)};
+ bridge_->installBaseModules(thread->state());
+ for (auto& module : modules_) {
+ bridge_->installPropertyModule(thread->state(), module.first,
+ module.second);
+ }
+ threads_.push_back(thread);
+ return thread;
+}
+
+auto Registry::AddPropertyModule(
+ const std::string& name,
+ std::vector<std::pair<std::string, std::variant<LuaFunction, Property*>>>
+ properties) -> void {
+ modules_.push_back(std::make_pair(name, properties));
+
+ // Any live threads will need to be updated to include the new module.
+ auto it = threads_.begin();
+ while (it != threads_.end()) {
+ auto thread = it->lock();
+ if (!thread) {
+ // Thread has been destroyed; stop tracking it.
+ it = threads_.erase(it);
+ } else {
+ bridge_->installPropertyModule(thread->state(), name, properties);
+ it++;
+ }
+ }
+}
+
+} // namespace lua