diff options
| author | Akshay Nair <phenax5@gmail.com> | 2025-04-14 00:44:15 +0530 |
|---|---|---|
| committer | Akshay Nair <phenax5@gmail.com> | 2025-04-14 00:44:15 +0530 |
| commit | 6869ca66e15259996c45501f1a40a0b354262815 (patch) | |
| tree | 13fc287f86f75c537e5b743918354a4d5b9b9699 | |
| parent | 9fc1e934cd2c58ed1ba03f860b09e7a8cd48e792 (diff) | |
| download | null-browser-6869ca66e15259996c45501f1a40a0b354262815.tar.gz null-browser-6869ca66e15259996c45501f1a40a0b354262815.zip | |
Refactor lua runtime api
| -rw-r--r-- | TODO.org | 66 | ||||
| -rw-r--r-- | src/LuaRuntime.cpp | 270 | ||||
| -rw-r--r-- | src/LuaRuntime.hpp | 34 | ||||
| -rw-r--r-- | src/LuaRuntimeApi.hpp | 205 |
4 files changed, 277 insertions, 298 deletions
@@ -1,60 +1,44 @@ -** Current -- [X] Process spawning lua (+ stdio) -- [X] Keybindings (lua api) -- [X] Modal keys -- [X] event loop for async work -- [X] example: trigger dmenu for simple selection -- [X] Fix segfault on quit -- [X] view history navigation -- [X] view next/prev -- [X] Get view list api -- [X] Assign ID to each view (reference in lua api) -- [X] view select by id -- [X] Multi-window -- [X] New window on new window request -- [X] Socket for opening window in current session (lua eval) -- [X] Config loading and lua path -- [X] Events/autocommands -- [X] Url changed event -- [X] History storage -- [X] History completion -- [X] Close window if last view closed -- [X] Rename terms: tab to view? (also web.tabs) -- [X] Configuration lua api -- [X] Switch to __internals -- [X] Respect cli args for main window -- [X] Downloading/download path config -- [ ] Use table for internals api options? +** Usable - [ ] Tests for api -- [ ] Log stdout, errors and results from lua somewhere -- [ ] Run JS in page -- [ ] INVESTIGATE: Check why urlchanged doesnt fire for first url open -- [ ] INVESTIGATE: Segfault on close -- [ ] INVESTIGATE: Errors in keymap/thread segfaults - -** Next - [ ] Search text in page - [ ] Dev tools - [ ] Fullscreen - [ ] Zoom in/out +- [ ] Scroll api (j/k/h/l/gg/G) - [ ] Permission requests handling/persisting - [ ] window.opener controls (use createWindow api directly?) - [ ] Notifications + +** Bugs +- [ ] INVESTIGATE: Check why urlchanged doesnt fire for first url open +- [ ] INVESTIGATE: Segfault on close +- [ ] INVESTIGATE: Errors in keymap/thread segfaults + +** Next +- [ ] Tests for window +- [ ] More tests for stack +- [ ] Tests for router +- [ ] Run JS in page +- [ ] Log stdout, errors and results from lua somewhere +- [ ] Use table for all internals api options? - [ ] Conflict in keymap (keymap already exists) - [ ] Allow pattern filtering for event listeners - [ ] Allow tab_id, win_id filtering for event listeners - [ ] Handle resource cleanup + signal disconnecting - [ ] Open url sanitize/humanize (add protocol if missing, remove quotes, etc) -- [ ] static linking for qt -- [X] static linking for libluv -- [ ] User scripts (greasemonkey?) -- [ ] User stylesheets (per site and global?) -- [ ] Bookmarking +- [ ] Profiles +- [ ] Private window (in-memory profile) +- [ ] Read page contents via lua ** Later +- [ ] Bookmarking +- [ ] User scripts (greasemonkey?) +- [ ] User stylesheets (per site and global?) - [ ] Support multiple modes for keymap.set: `web.keymap.set({'n','i'}, ...)` -- [ ] Read page contents via lua -- [ ] `web.to_string`: like vim.inspect +- [ ] static linking for qt +- [X] `web.inspect`: like vim.inspect +- [ ] vendor web.inspect with build +- [X] static linking for libluv - [X] Update window title on current webview title change ** Later later diff --git a/src/LuaRuntime.cpp b/src/LuaRuntime.cpp index 127e014..cb5a69a 100644 --- a/src/LuaRuntime.cpp +++ b/src/LuaRuntime.cpp @@ -9,29 +9,14 @@ extern "C" { #include "AsyncEventLoop.hpp" #include "LuaRuntime.hpp" -#include "WindowActionRouter.hpp" +#include "LuaRuntimeApi.hpp" LuaRuntime::LuaRuntime() { state = luaL_newstate(); luaL_openlibs(state); - preserve_top(state, { init_web_lib(); }); - - auto lua_path = QString(PROJECT_LUA_PATH); - preserve_top(state, { - lua_getglobal(state, "package"); - lua_getfield(state, -1, "path"); - auto pkg_path = QString(lua_tostring(state, -1)) + ";" + lua_path; - lua_pop(state, 1); - lua_pushstring(state, pkg_path.toStdString().c_str()); - lua_setfield(state, -2, "path"); - - lua_getglobal(state, "require"); - lua_pushstring(state, "null-browser.api"); - if (lua_pcall(state, 1, 0, 0) != LUA_OK) { - qCritical() << "Unable to load browser api" << lua_tostring(state, -1); - } - }); + init_lua_package_path(); + init_web_api(); } void LuaRuntime::start_event_loop() { @@ -59,6 +44,31 @@ void LuaRuntime::stop_event_loop() { lua_gc(state, LUA_GCCOLLECT, 0); } +void LuaRuntime::init_web_api() { + luaL_newlib(state, internals_api); + lua_setglobal(state, internals_global_name); + + preserve_top(state, { + lua_getglobal(state, "require"); + lua_pushstring(state, "null-browser.api"); + if (lua_pcall(state, 1, 0, 0) != LUA_OK) { + qCritical() << "Unable to load browser api" << lua_tostring(state, -1); + } + }); +} + +void LuaRuntime::init_lua_package_path() { + auto lua_path = QString(PROJECT_LUA_PATH); + preserve_top(state, { + lua_getglobal(state, "package"); + lua_getfield(state, -1, "path"); + auto pkg_path = QString(lua_tostring(state, -1)) + ";" + lua_path; + lua_pop(state, 1); + lua_pushstring(state, pkg_path.toStdString().c_str()); + lua_setfield(state, -2, "path"); + }); +} + void LuaRuntime::evaluate(const QString &code) { queue_task([this, code]() { preserve_top(state, { @@ -67,7 +77,7 @@ void LuaRuntime::evaluate(const QString &code) { qDebug() << "Lua Error: " << value; emit evaluation_failed(value); } else { - const QVariant value = get_lua_value(-1); + const QVariant value = get_lua_value(state, -1); emit evaluation_completed(value); } }) @@ -82,228 +92,6 @@ void LuaRuntime::load_file_sync(const QString &path) { }); } -int LuaRuntime::lua_event_register(lua_State *state) { - EventHandlerRequest event; - auto top = lua_gettop(state); - - lua_getfield(state, 1, "events"); - auto event_names = LuaRuntime::lua_tostringlist(state); - - event.event_names.swap(event_names); - if (event.event_names.size() == 0) { - lua_settop(state, top); - lua_pushboolean(state, false); - return 1; - } - - lua_getfield(state, 1, "patterns"); - auto patterns = LuaRuntime::lua_tostringlist(state); - event.patterns.swap(patterns); - - lua_getfield(state, 1, "callback"); - if (!lua_isfunction(state, -1)) { - lua_settop(state, top); - lua_pushboolean(state, false); - return 1; - } - - const int function_ref = luaL_ref(state, LUA_REGISTRYINDEX); - event.function_ref = function_ref; - // TODO: Delete ref on clear callback - event.handler = [state, function_ref](BrowserEvent *event) { - preserve_top(state, { - lua_rawgeti(state, LUA_REGISTRYINDEX, function_ref); - event->lua_push(state); - lua_pcall(state, 1, 0, 0); - }) - }; - - WindowActionRouter::instance().register_event(event); - - lua_settop(state, top); - lua_pushboolean(state, true); - return 1; -} - -void LuaRuntime::init_web_lib() { - // NOLINTBEGIN(modernize-avoid-c-arrays) - - luaL_Reg internals[] = { - {"event_add_listener", &LuaRuntime::lua_event_register}, - {"config_set", &LuaRuntime::lua_config_set}, - {"config_get", &LuaRuntime::lua_config_get}, - {"keymap_set", &LuaRuntime::lua_keymap_set}, - {"view_close", &LuaRuntime::lua_view_close}, - {"view_create", &LuaRuntime::lua_view_create}, - {"view_current", &LuaRuntime::lua_view_current}, - {"view_list", &LuaRuntime::lua_view_list}, - {"view_select", &LuaRuntime::lua_view_select}, - {"view_set_url", &LuaRuntime::lua_open_url}, - {"history_back", &LuaRuntime::lua_history_back}, - {"history_forward", &LuaRuntime::lua_history_forward}, - {nullptr, nullptr}, - }; - luaL_newlib(state, internals); - lua_setglobal(state, internals_global_name); - - // NOLINTEND(modernize-avoid-c-arrays) -} - -QVariant LuaRuntime::get_lua_value(int idx, QVariant default_value) { - if (lua_isnoneornil(state, idx)) - return default_value; - - if (lua_isstring(state, idx)) - return lua_tostring(state, idx); - - if (lua_isboolean(state, idx)) - return lua_toboolean(state, idx); - - if (lua_isnumber(state, idx)) - return lua_tonumber(state, idx); - - return lua_tostring(state, idx); -} - -int LuaRuntime::lua_open_url(lua_State *state) { - const char *url = lua_tostring(state, 1); - WebViewId view_id = lua_isnoneornil(state, 2) ? 0 : lua_tointeger(state, 2); - auto &runtime = LuaRuntime::instance(); - emit runtime.url_opened(url, OpenType::OpenUrl, view_id); - return 1; -} - -int LuaRuntime::lua_view_create(lua_State *state) { - const char *url = luaL_optstring(state, 1, ""); - auto &runtime = LuaRuntime::instance(); - emit runtime.url_opened(url, OpenType::OpenUrlInView, 0); - return 1; -} - -int LuaRuntime::lua_config_set(lua_State *state) { - const char *key = lua_tostring(state, 1); - auto &runtime = LuaRuntime::instance(); - QVariant value = runtime.get_lua_value(2, ""); - emit runtime.config_updated(key, value); - return 1; -} - -int LuaRuntime::lua_config_get(lua_State *state) { - const char *key = lua_tostring(state, 1); - auto &router = WindowActionRouter::instance(); - auto value = router.fetch_config_value(key); - - if (value.typeId() == QMetaType::QString) - lua_pushstring(state, value.toString().toStdString().c_str()); - else if (value.typeId() == QMetaType::Int) - lua_pushinteger(state, value.toInt()); - else if (value.typeId() == QMetaType::Double) - lua_pushnumber(state, value.toDouble()); - else - lua_pushnil(state); - - return 1; -} - -int LuaRuntime::lua_keymap_set(lua_State *state) { - const char *mode = lua_tostring(state, 1); - const char *keyseq = lua_tostring(state, 2); - - lua_pushvalue(state, 3); - const int function_ref = luaL_ref(state, LUA_REGISTRYINDEX); - auto action = [state, function_ref]() { - preserve_top(state, { - lua_rawgeti(state, LUA_REGISTRYINDEX, function_ref); - if (lua_pcall(state, 0, 0, 0) != LUA_OK) { - const char *error = lua_tostring(state, -1); - qDebug() << "Error calling Lua function:" << error; - } - }) - }; - // TODO: Cleanup function ref on after keymap clear - - auto &runtime = LuaRuntime::instance(); - emit runtime.keymap_added(mode, keyseq, action); - - return 1; -} - -int LuaRuntime::lua_view_current(lua_State *state) { - auto &router = WindowActionRouter::instance(); - auto view_id = router.fetch_current_view_id(); - lua_pushinteger(state, view_id); - return 1; -} - -int LuaRuntime::lua_view_list(lua_State *state) { - auto &router = WindowActionRouter::instance(); - auto views = router.fetch_webview_data_list(); - lua_newtable(state); - - int index = 1; // 1-indexed - for (auto &view : views) { - lua_newtable(state); - - lua_pushstring(state, "id"); - lua_pushinteger(state, view.id); - lua_settable(state, -3); - - lua_pushstring(state, "url"); - lua_pushstring(state, view.url.toStdString().c_str()); - lua_settable(state, -3); - - lua_pushstring(state, "title"); - lua_pushstring(state, view.title.toStdString().c_str()); - lua_settable(state, -3); - - lua_rawseti(state, -2, index); - index++; - } - - return 1; -} - -int LuaRuntime::lua_history_back(lua_State *state) { - auto &runtime = LuaRuntime::instance(); - - qsizetype history_index = lua_isnoneornil(state, 1) ? 1 : lua_tointeger(state, 1); - - WebViewId view_id = lua_isnoneornil(state, 2) ? 0 : lua_tointeger(state, 2); - - emit runtime.history_back_requested(view_id, history_index); - return 1; -} - -int LuaRuntime::lua_history_forward(lua_State *state) { - auto &runtime = LuaRuntime::instance(); - - qsizetype history_index = lua_isnoneornil(state, 1) ? 1 : lua_tointeger(state, 1); - - WebViewId view_id = lua_isnoneornil(state, 2) ? 0 : lua_tointeger(state, 2); - - emit runtime.history_forward_requested(view_id, history_index); - return 1; -} - -int LuaRuntime::lua_view_close(lua_State *state) { - auto &runtime = LuaRuntime::instance(); - - WebViewId view_id = lua_isnoneornil(state, 1) ? 0 : lua_tointeger(state, 1); - - emit runtime.webview_closed(view_id); - return 1; -} - -int LuaRuntime::lua_view_select(lua_State *state) { - if (lua_isnoneornil(state, 1)) - return 1; // TODO: return nil (for others too) - - auto &runtime = LuaRuntime::instance(); - WebViewId view_id = lua_tointeger(state, 1); - emit runtime.webview_selected(view_id); - return 1; -} - LuaRuntime::~LuaRuntime() { stop_event_loop(); lua_close(state); diff --git a/src/LuaRuntime.hpp b/src/LuaRuntime.hpp index 4978c74..f684409 100644 --- a/src/LuaRuntime.hpp +++ b/src/LuaRuntime.hpp @@ -39,7 +39,6 @@ public: void start_event_loop(); DELEGATE(event_loop, queue_task, queue_task) - QVariant get_lua_value(int idx, QVariant default_value = 0); DEFINE_GETTER(get_state, state) signals: @@ -57,21 +56,8 @@ signals: protected: LuaRuntime(); ~LuaRuntime() override; - void init_web_lib(); - - // Lua api - static int lua_history_back(lua_State *state); - static int lua_history_forward(lua_State *state); - static int lua_event_register(lua_State *state); - static int lua_keymap_set(lua_State *state); - static int lua_open_url(lua_State *state); - static int lua_view_close(lua_State *state); - static int lua_view_create(lua_State *state); - static int lua_view_current(lua_State *state); - static int lua_view_list(lua_State *state); - static int lua_view_select(lua_State *state); - static int lua_config_set(lua_State *state); - static int lua_config_get(lua_State *state); + void init_lua_package_path(); + void init_web_api(); private: lua_State *state; @@ -107,4 +93,20 @@ public: return values; } + + static QVariant get_lua_value(lua_State *state, int idx, QVariant default_value = 0) { + if (lua_isnoneornil(state, idx)) + return default_value; + + if (lua_isstring(state, idx)) + return lua_tostring(state, idx); + + if (lua_isboolean(state, idx)) + return lua_toboolean(state, idx); + + if (lua_isnumber(state, idx)) + return lua_tonumber(state, idx); + + return lua_tostring(state, idx); + } }; diff --git a/src/LuaRuntimeApi.hpp b/src/LuaRuntimeApi.hpp new file mode 100644 index 0000000..809b579 --- /dev/null +++ b/src/LuaRuntimeApi.hpp @@ -0,0 +1,205 @@ +#pragma once + +#include <lua.hpp> + +#include "LuaRuntime.hpp" +#include "WindowActionRouter.hpp" + +int lua_api_open_url(lua_State *state) { + const char *url = lua_tostring(state, 1); + WebViewId view_id = lua_isnoneornil(state, 2) ? 0 : lua_tointeger(state, 2); + auto &runtime = LuaRuntime::instance(); + emit runtime.url_opened(url, OpenType::OpenUrl, view_id); + return 1; +} + +int lua_api_view_create(lua_State *state) { + const char *url = luaL_optstring(state, 1, ""); + auto &runtime = LuaRuntime::instance(); + emit runtime.url_opened(url, OpenType::OpenUrlInView, 0); + return 1; +} + +int lua_api_config_set(lua_State *state) { + const char *key = lua_tostring(state, 1); + auto &runtime = LuaRuntime::instance(); + QVariant value = LuaRuntime::get_lua_value(runtime.get_state(), 2, ""); + emit runtime.config_updated(key, value); + return 1; +} + +int lua_api_config_get(lua_State *state) { + const char *key = lua_tostring(state, 1); + auto &router = WindowActionRouter::instance(); + auto value = router.fetch_config_value(key); + + if (value.typeId() == QMetaType::QString) + lua_pushstring(state, value.toString().toStdString().c_str()); + else if (value.typeId() == QMetaType::Int) + lua_pushinteger(state, value.toInt()); + else if (value.typeId() == QMetaType::Double) + lua_pushnumber(state, value.toDouble()); + else + lua_pushnil(state); + + return 1; +} + +int lua_api_keymap_set(lua_State *state) { + const char *mode = lua_tostring(state, 1); + const char *keyseq = lua_tostring(state, 2); + + lua_pushvalue(state, 3); + const int function_ref = luaL_ref(state, LUA_REGISTRYINDEX); + auto action = [state, function_ref]() { + preserve_top(state, { + lua_rawgeti(state, LUA_REGISTRYINDEX, function_ref); + if (lua_pcall(state, 0, 0, 0) != LUA_OK) { + const char *error = lua_tostring(state, -1); + qDebug() << "Error calling Lua function:" << error; + } + }) + }; + // TODO: Cleanup function ref on after keymap clear + + auto &runtime = LuaRuntime::instance(); + emit runtime.keymap_added(mode, keyseq, action); + + return 1; +} + +int lua_api_view_current(lua_State *state) { + auto &router = WindowActionRouter::instance(); + auto view_id = router.fetch_current_view_id(); + lua_pushinteger(state, view_id); + return 1; +} + +int lua_view_list(lua_State *state) { + auto &router = WindowActionRouter::instance(); + auto views = router.fetch_webview_data_list(); + lua_newtable(state); + + int index = 1; // 1-indexed + for (auto &view : views) { + lua_newtable(state); + + lua_pushstring(state, "id"); + lua_pushinteger(state, view.id); + lua_settable(state, -3); + + lua_pushstring(state, "url"); + lua_pushstring(state, view.url.toStdString().c_str()); + lua_settable(state, -3); + + lua_pushstring(state, "title"); + lua_pushstring(state, view.title.toStdString().c_str()); + lua_settable(state, -3); + + lua_rawseti(state, -2, index); + index++; + } + + return 1; +} + +int lua_history_back(lua_State *state) { + auto &runtime = LuaRuntime::instance(); + + qsizetype history_index = lua_isnoneornil(state, 1) ? 1 : lua_tointeger(state, 1); + + WebViewId view_id = lua_isnoneornil(state, 2) ? 0 : lua_tointeger(state, 2); + + emit runtime.history_back_requested(view_id, history_index); + return 1; +} + +int lua_history_forward(lua_State *state) { + auto &runtime = LuaRuntime::instance(); + + qsizetype history_index = lua_isnoneornil(state, 1) ? 1 : lua_tointeger(state, 1); + + WebViewId view_id = lua_isnoneornil(state, 2) ? 0 : lua_tointeger(state, 2); + + emit runtime.history_forward_requested(view_id, history_index); + return 1; +} + +int lua_view_close(lua_State *state) { + auto &runtime = LuaRuntime::instance(); + + WebViewId view_id = lua_isnoneornil(state, 1) ? 0 : lua_tointeger(state, 1); + + emit runtime.webview_closed(view_id); + return 1; +} + +int lua_view_select(lua_State *state) { + if (lua_isnoneornil(state, 1)) + return 1; // TODO: return nil (for others too) + + auto &runtime = LuaRuntime::instance(); + WebViewId view_id = lua_tointeger(state, 1); + emit runtime.webview_selected(view_id); + return 1; +} + +int lua_event_register(lua_State *state) { + EventHandlerRequest event; + auto top = lua_gettop(state); + + lua_getfield(state, 1, "events"); + auto event_names = LuaRuntime::lua_tostringlist(state); + + event.event_names.swap(event_names); + if (event.event_names.size() == 0) { + lua_settop(state, top); + lua_pushboolean(state, false); + return 1; + } + + lua_getfield(state, 1, "patterns"); + auto patterns = LuaRuntime::lua_tostringlist(state); + event.patterns.swap(patterns); + + lua_getfield(state, 1, "callback"); + if (!lua_isfunction(state, -1)) { + lua_settop(state, top); + lua_pushboolean(state, false); + return 1; + } + + const int function_ref = luaL_ref(state, LUA_REGISTRYINDEX); + event.function_ref = function_ref; + // TODO: Delete ref on clear callback + event.handler = [state, function_ref](BrowserEvent *event) { + preserve_top(state, { + lua_rawgeti(state, LUA_REGISTRYINDEX, function_ref); + event->lua_push(state); + lua_pcall(state, 1, 0, 0); + }) + }; + + WindowActionRouter::instance().register_event(event); + + lua_settop(state, top); + lua_pushboolean(state, true); + return 1; +} + +// NOLINTNEXTLINE +static luaL_Reg internals_api[] = { + luaL_Reg{"event_add_listener", &lua_event_register}, + luaL_Reg{"config_set", &lua_api_config_set}, + luaL_Reg{"config_get", &lua_api_config_get}, + luaL_Reg{"keymap_set", &lua_api_keymap_set}, + luaL_Reg{"view_close", &lua_view_close}, + luaL_Reg{"view_create", &lua_api_view_create}, + luaL_Reg{"view_current", &lua_api_view_current}, + luaL_Reg{"view_list", &lua_view_list}, + luaL_Reg{"view_select", &lua_view_select}, + luaL_Reg{"view_set_url", &lua_api_open_url}, + luaL_Reg{"history_back", &lua_history_back}, + luaL_Reg{"history_forward", &lua_history_forward}, + luaL_Reg{nullptr, nullptr}, +}; |
