aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAkshay Nair <phenax5@gmail.com>2025-04-14 00:44:15 +0530
committerAkshay Nair <phenax5@gmail.com>2025-04-14 00:44:15 +0530
commit6869ca66e15259996c45501f1a40a0b354262815 (patch)
tree13fc287f86f75c537e5b743918354a4d5b9b9699
parent9fc1e934cd2c58ed1ba03f860b09e7a8cd48e792 (diff)
downloadnull-browser-6869ca66e15259996c45501f1a40a0b354262815.tar.gz
null-browser-6869ca66e15259996c45501f1a40a0b354262815.zip
Refactor lua runtime api
-rw-r--r--TODO.org66
-rw-r--r--src/LuaRuntime.cpp270
-rw-r--r--src/LuaRuntime.hpp34
-rw-r--r--src/LuaRuntimeApi.hpp205
4 files changed, 277 insertions, 298 deletions
diff --git a/TODO.org b/TODO.org
index dc8b1d2..8d4a3e5 100644
--- a/TODO.org
+++ b/TODO.org
@@ -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},
+};