aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAkshay Nair <phenax5@gmail.com>2025-04-02 20:49:49 +0530
committerAkshay Nair <phenax5@gmail.com>2025-04-02 20:49:49 +0530
commit96727d6e63ca927f3c7b68d4baa4fe672a4dcd0b (patch)
tree2e5b99bb3324a35a0bd9ecdaa2caefcf9260767a
parent4f945367ebc8e34263acbfca0416e3f75653924e (diff)
downloadnull-browser-96727d6e63ca927f3c7b68d4baa4fe672a4dcd0b.tar.gz
null-browser-96727d6e63ca927f3c7b68d4baa4fe672a4dcd0b.zip
Add events system for lua runtime to dispatch and register events
-rw-r--r--TODO.org1
-rw-r--r--spec/LuaRuntimeSpec.cpp136
-rw-r--r--spec/testUtils.cpp12
-rw-r--r--spec/testUtils.h2
-rw-r--r--src/LuaRuntime.cpp58
-rw-r--r--src/LuaRuntime.hpp36
-rw-r--r--src/WindowActionRouter.cpp37
-rw-r--r--src/WindowActionRouter.hpp10
-rw-r--r--src/events.hpp47
-rw-r--r--src/widgets/BrowserApp.cpp14
-rw-r--r--src/widgets/BrowserWindow.hpp3
11 files changed, 332 insertions, 24 deletions
diff --git a/TODO.org b/TODO.org
index bee6eb2..55daa28 100644
--- a/TODO.org
+++ b/TODO.org
@@ -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;