diff options
| author | Akshay Nair <phenax5@gmail.com> | 2025-04-02 20:49:49 +0530 |
|---|---|---|
| committer | Akshay Nair <phenax5@gmail.com> | 2025-04-02 20:49:49 +0530 |
| commit | 96727d6e63ca927f3c7b68d4baa4fe672a4dcd0b (patch) | |
| tree | 2e5b99bb3324a35a0bd9ecdaa2caefcf9260767a | |
| parent | 4f945367ebc8e34263acbfca0416e3f75653924e (diff) | |
| download | null-browser-96727d6e63ca927f3c7b68d4baa4fe672a4dcd0b.tar.gz null-browser-96727d6e63ca927f3c7b68d4baa4fe672a4dcd0b.zip | |
Add events system for lua runtime to dispatch and register events
Diffstat (limited to '')
| -rw-r--r-- | TODO.org | 1 | ||||
| -rw-r--r-- | spec/LuaRuntimeSpec.cpp | 136 | ||||
| -rw-r--r-- | spec/testUtils.cpp | 12 | ||||
| -rw-r--r-- | spec/testUtils.h | 2 | ||||
| -rw-r--r-- | src/LuaRuntime.cpp | 58 | ||||
| -rw-r--r-- | src/LuaRuntime.hpp | 36 | ||||
| -rw-r--r-- | src/WindowActionRouter.cpp | 37 | ||||
| -rw-r--r-- | src/WindowActionRouter.hpp | 10 | ||||
| -rw-r--r-- | src/events.hpp | 47 | ||||
| -rw-r--r-- | src/widgets/BrowserApp.cpp | 14 | ||||
| -rw-r--r-- | src/widgets/BrowserWindow.hpp | 3 |
11 files changed, 332 insertions, 24 deletions
@@ -11,6 +11,7 @@ - [X] Assign ID to each tab (reference in lua api) - [X] Tab select by id - [X] Multi-window +- [ ] New window on new window request - [ ] Socket for opening window in current session (lua eval) - [ ] Events/autocommands - [ ] History storage diff --git a/spec/LuaRuntimeSpec.cpp b/spec/LuaRuntimeSpec.cpp index b5dd914..9460cae 100644 --- a/spec/LuaRuntimeSpec.cpp +++ b/spec/LuaRuntimeSpec.cpp @@ -2,8 +2,20 @@ #include <uv.h> #include "LuaRuntime.hpp" +#include "WindowActionRouter.hpp" +#include "events.hpp" #include "testUtils.h" +class TestEvent1 : public BrowserEvent { +public: + int num; + TestEvent1(int num) : num(num) { kind = "TestEvent1"; } + void lua_push(lua_State *state) override { + lua_newtable(state); + SET_FIELD("test_data", integer, num); + } +}; + // NOLINTBEGIN class LuaRuntimeSpec : public QObject { Q_OBJECT @@ -85,7 +97,6 @@ private slots: QVERIFY( QTest::qWaitFor([&was_task_called]() { return was_task_called; })); - QVERIFY(was_task_called); } } @@ -105,11 +116,8 @@ private slots: end) )"); QVERIFY(evaluation_completed_spy.wait()); - QVERIFY(NOT lua.evaluate_sync("return _G.was_timer_called").toBool()); - QVERIFY(QTest::qWaitFor([&lua]() { - return lua.evaluate_sync("return _G.was_timer_called").toBool(); - })); + QVERIFY(wait_for_lua_to_be_true("return _G.was_timer_called")); } } @@ -126,11 +134,121 @@ private slots: end) )"); QVERIFY(evaluation_completed_spy.wait()); - QCOMPARE(lua.evaluate_sync("return _G.spawn_exit_code").toInt(), -1); - QVERIFY(QTest::qWaitFor([&lua]() { - return 0 == lua.evaluate_sync("return _G.spawn_exit_code").toInt(); - })); + QVERIFY(wait_for_lua_to_be_true("return _G.spawn_exit_code == 0")); + } + } + + void test_internals_register_event() { + context("when events, patterns and callback are specified correctly"); + it("returns true and registers event") { + auto &lua = LuaRuntime::instance(); + QSignalSpy evaluation_completed_spy(&lua, + &LuaRuntime::evaluation_completed); + + lua.evaluate(R"( + return __internals.register_event({ + events = { 'Hello', 'World' }, + patterns = { 'p1', 'p2' }, + callback = function() print("Called") end, + }); + )"); + evaluation_completed_spy.wait(); + + QCOMPARE(evaluation_completed_spy.count(), 1); + QVariant result = evaluation_completed_spy.takeFirst().at(0); + QCOMPARE(result, true); + } + + context("when patterns is not passed"); + it("returns true and registers event") { + auto &lua = LuaRuntime::instance(); + QSignalSpy evaluation_completed_spy(&lua, + &LuaRuntime::evaluation_completed); + + lua.evaluate(R"( + return __internals.register_event({ + events = { 'Hello', 'World' }, + callback = function() print("Called") end, + }); + )"); + evaluation_completed_spy.wait(); + + QCOMPARE(evaluation_completed_spy.count(), 1); + QVariant result = evaluation_completed_spy.takeFirst().at(0); + QCOMPARE(result, true); + } + + context("when events list is not passed"); + it("returns false and doesnt register event") { + auto &lua = LuaRuntime::instance(); + QSignalSpy evaluation_completed_spy(&lua, + &LuaRuntime::evaluation_completed); + + lua.evaluate(R"( + return __internals.register_event({ + patterns = { 'p1', 'p2' }, + callback = function() print("Called") end, + }); + )"); + evaluation_completed_spy.wait(); + + QCOMPARE(evaluation_completed_spy.count(), 1); + QVariant result = evaluation_completed_spy.first().first(); + QCOMPARE(result, false); + } + + context("when callback is not passed"); + it("returns false and doesnt register event") { + auto &lua = LuaRuntime::instance(); + QSignalSpy evaluation_completed_spy(&lua, + &LuaRuntime::evaluation_completed); + + lua.evaluate(R"( + return __internals.register_event({ + events = {'Ev'}, + patterns = { 'p1', 'p2' }, + }); + )"); + evaluation_completed_spy.wait(); + + QCOMPARE(evaluation_completed_spy.count(), 1); + QVariant result = evaluation_completed_spy.first().first(); + QCOMPARE(result, false); + } + } + + void test_internals_register_event_handler() { + context("when dispatching a registered event (without pattern)"); + it("calls the registered event handler") { + auto &lua = LuaRuntime::instance(); + QSignalSpy evaluation_completed_spy(&lua, + &LuaRuntime::evaluation_completed); + lua.evaluate(R"( + _G.event1_called = false; + _G.event1_called_with = nil; + __internals.register_event({ + events = { 'TestEvent1' }, + callback = function(opts) + _G.event1_called = true + _G.event1_called_with = opts + end, + }); + _G.event2_called = false; + __internals.register_event({ + events = { 'TestEvent2' }, + callback = function(opts) _G.event2_called = true end, + }); + )"); + evaluation_completed_spy.wait(); + + TestEvent1 event(42); + WindowActionRouter::instance().dispatch_event(event); + + QVERIFY(wait_for_lua_to_be_true("return _G.event1_called")); + QVERIFY(wait_for_lua_to_be_true( + "return _G.event1_called_with.test_data == 42")); + QVERIFY(wait_for_lua_to_be_true("return not _G.event2_called")); } } }; diff --git a/spec/testUtils.cpp b/spec/testUtils.cpp index cea7a3d..8f348bd 100644 --- a/spec/testUtils.cpp +++ b/spec/testUtils.cpp @@ -1,3 +1,4 @@ +#include "LuaRuntime.hpp" #include <QtTest/QtTest> #include <QtTest/qtestcase.h> #include <cstdio> @@ -22,3 +23,14 @@ int run_all_tests() { get_qtest_registry().clear(); return exit_code; } + +bool wait_for_lua_to_be_true(QString lua_code) { + return QTest::qWaitFor([&lua_code]() { + auto &lua = LuaRuntime::instance(); + QSignalSpy evaluation_completed_spy(&lua, + &LuaRuntime::evaluation_completed); + lua.evaluate(lua_code); + evaluation_completed_spy.wait(); + return evaluation_completed_spy.first().first().toBool(); + }); +} diff --git a/spec/testUtils.h b/spec/testUtils.h index f6cdebb..0ba2e9a 100644 --- a/spec/testUtils.h +++ b/spec/testUtils.h @@ -40,3 +40,5 @@ int run_all_tests(); return true; \ }(); \ }; + +bool wait_for_lua_to_be_true(QString lua_code); diff --git a/src/LuaRuntime.cpp b/src/LuaRuntime.cpp index 37349b0..f25b73b 100644 --- a/src/LuaRuntime.cpp +++ b/src/LuaRuntime.cpp @@ -1,4 +1,5 @@ #include "WindowActionRouter.hpp" +#include "lua.h" #include <QtCore> #include <lua.hpp> extern "C" { @@ -11,6 +12,7 @@ extern "C" { LuaRuntime::LuaRuntime() { state = luaL_newstate(); luaL_openlibs(state); + preserve_top(state, { init_web_lib(); }) } @@ -48,18 +50,12 @@ void LuaRuntime::evaluate(const QString &code) { emit evaluation_failed(value); } else { const QVariant value = get_lua_value(-1); - qDebug() << "result: " << value; emit evaluation_completed(value); } }) }); } -QVariant LuaRuntime::evaluate_sync(const QString &code) { - luaL_dostring(state, code.toStdString().c_str()); - return get_lua_value(-1); // TODO: error handling -} - void LuaRuntime::load_file(const QString &path) { queue_task([this, path]() { preserve_top(state, { @@ -70,9 +66,59 @@ void LuaRuntime::load_file(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_call(state, 1, 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[] = { + {"register_event", &LuaRuntime::lua_event_register}, + {nullptr, nullptr}, + }; + luaL_newlib(state, internals); + lua_setglobal(state, internals_global_name); + // web luaL_Reg web[] = { /// @deprecated diff --git a/src/LuaRuntime.hpp b/src/LuaRuntime.hpp index d2211df..074b76a 100644 --- a/src/LuaRuntime.hpp +++ b/src/LuaRuntime.hpp @@ -3,8 +3,10 @@ #include <QtCore> #include <functional> #include <lua.hpp> +#include <string> #include "AsyncEventLoop.hpp" +#include "lua.h" #include "utils.hpp" #include "widgets/WebViewStack.hpp" @@ -18,6 +20,7 @@ class LuaRuntime : public QObject { const char *uv_global_name = "uv"; const char *web_global_name = "web"; + const char *internals_global_name = "__internals"; public: static LuaRuntime &instance() { @@ -27,7 +30,6 @@ public: void evaluate(const QString &code); void load_file(const QString &path); - QVariant evaluate_sync(const QString &code); void stop_event_loop(); void start_event_loop(); @@ -55,6 +57,7 @@ protected: // 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_tab_close(lua_State *state); @@ -66,4 +69,35 @@ protected: private: lua_State *state; AsyncEventLoop *event_loop = nullptr; + + // TEMP +public: + static void inspect_lua_stack(lua_State *state) { + int top = lua_gettop(state); + qDebug() << "Lua Stack (top: " << top << "):\n"; + + for (int i = 1; i <= top; i++) { + int type = lua_type(state, i); + qDebug() << i << ": " << lua_typename(state, type); + } + + qDebug() << "---------------------------\n"; + lua_settop(state, top); + } + + static std::vector<std::string> lua_tostringlist(lua_State *state) { + std::vector<std::string> values; + if (!lua_istable(state, -1)) + return values; + + lua_pushnil(state); // First key for lua_next() + while (lua_next(state, -2) != 0) { + if (lua_isstring(state, -1)) + values.emplace_back(lua_tostring(state, -1)); + lua_pop(state, 1); + } + lua_pop(state, 1); + + return values; + } }; diff --git a/src/WindowActionRouter.cpp b/src/WindowActionRouter.cpp index 1c5164e..89f92c7 100644 --- a/src/WindowActionRouter.cpp +++ b/src/WindowActionRouter.cpp @@ -1,12 +1,49 @@ #include <QWidget> #include <QtCore> +#include <string> +#include <unordered_map> #include "LuaRuntime.hpp" #include "WindowActionRouter.hpp" #include "keymap/KeymapEvaluator.hpp" +#include "widgets/BrowserWindow.hpp" #include "widgets/WebViewStack.hpp" +void WindowActionRouter::dispatch_event(BrowserEvent &event) { + auto &runtime = LuaRuntime::instance(); + runtime.queue_task([this, &event]() { + std::unordered_map<std::string, std::vector<EventHandlerRequest>> event_map; + { + const std::lock_guard<std::mutex> lock(events_mutex); + event_map = events; + } + + if (!event_map.contains(event.kind)) + return; + + for (auto &event_handler : event_map.at(event.kind)) { + // TODO: Pattern filter + event_handler.handler(event); + } + }); +} + +void WindowActionRouter::register_event(const EventHandlerRequest &event) { + const std::lock_guard<std::mutex> lock(events_mutex); + for (const auto &event_name : event.event_names) { + if (!events.contains(event_name)) + events.insert({event_name, {}}); + events.at(event_name).emplace_back(event); + } +} + void WindowActionRouter::initialize() { + UrlChangedEvent url_changed_event("https://googa.com", 2, 1); + WindowActionRouter::instance().dispatch_event(url_changed_event); + + UrlChangedEvent url_changed_event_1("https://duckduckgo.com", 5, 1); + WindowActionRouter::instance().dispatch_event(url_changed_event_1); + auto &runtime = LuaRuntime::instance(); connect(&runtime, &LuaRuntime::keymap_added, this, diff --git a/src/WindowActionRouter.hpp b/src/WindowActionRouter.hpp index 384c76c..62aa65d 100644 --- a/src/WindowActionRouter.hpp +++ b/src/WindowActionRouter.hpp @@ -4,7 +4,11 @@ #include <QtCore> #include <cstdint> #include <functional> +#include <mutex> +#include <string> +#include <unordered_map> +#include "events.hpp" #include "widgets/BrowserWindow.hpp" #include "widgets/WebViewStack.hpp" @@ -33,6 +37,9 @@ public: WebViewId fetch_current_tab_id(WindowId win_id = 0); QList<WebViewData> fetch_webview_data_list(WindowId win_id = 0); + void dispatch_event(BrowserEvent &event); + void register_event(const EventHandlerRequest &event); + protected: WindowActionRouter() = default; @@ -42,4 +49,7 @@ protected: private: WindowMap window_map; uint64_t last_id = 1; + + std::mutex events_mutex; + std::unordered_map<std::string, std::vector<EventHandlerRequest>> events; }; diff --git a/src/events.hpp b/src/events.hpp new file mode 100644 index 0000000..6d10fc7 --- /dev/null +++ b/src/events.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include <QtCore> +#include <lua.hpp> + +#include "lua.h" +#include "widgets/BrowserWindow.hpp" +#include "widgets/WebViewStack.hpp" + +#define SET_FIELD(NAME, TYPE, VALUE) \ + lua_pushstring(state, NAME); \ + lua_push##TYPE(state, VALUE); \ + lua_settable(state, -3); + +class BrowserEvent { +public: + std::string kind = "-"; + virtual ~BrowserEvent() = default; + + virtual void lua_push(lua_State *state) { lua_newtable(state); }; +}; + +class UrlChangedEvent : public BrowserEvent { +public: + const QString url; + const WebViewId webview_id; + const WindowId win_id; + + UrlChangedEvent(QString url, WebViewId webview_id, WindowId win_id) + : url(std::move(url)), webview_id(webview_id), win_id(win_id) { + kind = "UrlChanged"; + } + + void lua_push(lua_State *state) override { + lua_newtable(state); + SET_FIELD("tab", integer, webview_id) + SET_FIELD("win", integer, win_id) + SET_FIELD("url", string, url.toStdString().c_str()) + } +}; + +struct EventHandlerRequest { + std::vector<std::string> event_names; + std::vector<std::string> patterns; + std::function<void(BrowserEvent &)> handler; + int function_ref; +}; diff --git a/src/widgets/BrowserApp.cpp b/src/widgets/BrowserApp.cpp index f2d541d..d65a122 100644 --- a/src/widgets/BrowserApp.cpp +++ b/src/widgets/BrowserApp.cpp @@ -11,8 +11,7 @@ BrowserApp::BrowserApp() { lua.start_event_loop(); // Router init - auto &router = WindowActionRouter::instance(); - router.initialize(); + WindowActionRouter::instance().initialize(); // Global event filter qApp->installEventFilter(this); @@ -22,14 +21,15 @@ BrowserApp::BrowserApp() { }; BrowserWindow *BrowserApp::create_window() { - auto *win = new BrowserWindow((const Configuration &)configuration); - win->setWindowTitle("null-browser"); - WindowActionRouter::instance().add_window(win); - win->show(); - return win; + auto *window = new BrowserWindow((const Configuration &)configuration); + window->setWindowTitle("null-browser"); + WindowActionRouter::instance().add_window(window); + window->show(); + return window; } bool BrowserApp::eventFilter(QObject *target, QEvent *event) { + // TODO: Prevent key release and shortcut on mode too if (event->type() != QEvent::KeyPress) return false; diff --git a/src/widgets/BrowserWindow.hpp b/src/widgets/BrowserWindow.hpp index 9aeeda3..4865ec9 100644 --- a/src/widgets/BrowserWindow.hpp +++ b/src/widgets/BrowserWindow.hpp @@ -6,7 +6,7 @@ #include "WindowMediator.hpp" #include "utils.hpp" -using WindowId = uint64_t; +using WindowId = qsizetype; class BrowserWindow : public QMainWindow { Q_OBJECT @@ -24,6 +24,7 @@ public: signals: void closed(); + // void new_window_requested(const QUrl &url); private: WindowMediator *win_mediator; |
