From 46a7fee95a010f20490d7f66f0c2c82ed936ca5e Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Thu, 27 Mar 2025 13:08:20 +0530 Subject: Refactor and fix tab id use for focus/close --- TODO.org | 70 +++++++++++++++++++------ config.lua | 57 ++++++++++++-------- spec/WebViewStackSpec.cpp | 26 ++++----- src/InputMediator.cpp | 7 ++- src/InputMediator.hpp | 5 +- src/LuaRuntime.cpp | 122 +++++++++++++++++++++---------------------- src/LuaRuntime.hpp | 21 ++++---- src/main.cpp | 1 - src/widgets/MainWindow.cpp | 4 +- src/widgets/WebViewStack.cpp | 1 + src/widgets/WebViewStack.hpp | 5 +- 11 files changed, 188 insertions(+), 131 deletions(-) diff --git a/TODO.org b/TODO.org index a2b3749..aae520b 100644 --- a/TODO.org +++ b/TODO.org @@ -5,35 +5,75 @@ - [X] event loop for async work - [X] example: trigger dmenu for simple selection - [X] Fix segfault on quit -- [ ] Tab history navigation -- [ ] Tab next/prev -- [ ] Get tab list api -- [ ] Assign ID to each tab (reference in lua api) -- [ ] Tab select by id -- [ ] Config loading and runtime path +- [X] Tab history navigation +- [X] Tab next/prev +- [X] Get tab list api +- [X] Assign ID to each tab (reference in lua api) +- [X] Tab select by id - [ ] Multi-window +- [ ] Handle resource cleanup + signal disconnecting +- [ ] Socket for opening window in current session (lua eval) +- [ ] Log stdout, errors and results from lua somewhere +- [ ] Events/autocommands +- [ ] History storage +- [ ] History completion ** Next +- [ ] Config loading and runtime path - [ ] Conflict in keymap (keymap already exists) -- [ ] History persistance -- [ ] History completion -- [ ] Open url parsing (add protocol if missing, remove quotes, etc) -- [ ] Destructor for completer instance +- [ ] Open url sanitize/humanize (add protocol if missing, remove quotes, etc) - [ ] Bookmarking +- [ ] Downloading ** Later - [ ] Support multiple modes for keymap.set: `web.keymap.set({'n','i'}, ...)` -- [ ] Search aliasing (lua api) -- [ ] downloading - [ ] Search text in page - [ ] Notifications - [ ] Dev tools -- [ ] Search engine search if its not a url / aliases +- [ ] Permission requests handling +- [ ] Pipe page contents to external program +- [ ] `web.inspect` ** Later later - [ ] Fullscreen +- [ ] Custom file picker - [ ] remote debugging with cdp (spider-repl) -- [ ] Pipe page contents to external program ** Maybe -- [ ] Custom file picker +- [ ] Floating webviews? +- [ ] Move modal keymap management into lua? + +** Notes +- Tab = 0, nil, none: Current tab +- Win = 0: Current window +- Win = nil, none: All windows or current window based on operation +- Keep track of last focused window (current window is last focused) +#+begin_src lua +web.tab.set_url(url) -- Set url for current tab +web.tab.set_url(url, { tab = 1 }) -- Set url for tab 1 +web.tab.set_url(url, { tab = 1, win = 1 }) -- Set url for tab 1 in win 1 +web.tab.set_url(url, { win = 1 }) -- Set url for current tab in win 1 + +web.tab.new(url, { win = 1 }) -- New tab in win 1 +web.tab.new(url) -- New tab in current window + +web.tab.close({ tab = 1 }) -- Close tab 1 +web.tab.close() -- Close current tab in current window + +web.tab.list({ win = nil }) -- List all tabs +web.tab.list({ win = 1 }) -- List win 1 tabs + +web.tab.select({ tab = 1, win = 1 }) -- Select win 1 tab +web.tab.select({ tab = 1, win = 0 }) -- Select tab 1 in current window +web.tab.select({ tab = 1 }) -- Select tab 1 + +web.tab.current({ win = 1 }) -- Current tab in win 1 +web.tab.current() -- Current tab in current win + +web.history.back(1) -- Back for current tab in current window +web.history.back(1, { tab = 1 }) -- Back for tab 1 +web.history.back(1, { win = 1 }) -- Back for current tab in win 1 + +web.keymap.set('n', '', ..., { tab = 1 }) -- Set keymap for tab 1 +web.keymap.set('n', '', ..., { win = 1 }) -- Set keymap for win 1 +#+end_src diff --git a/config.lua b/config.lua index 053db68..b98c4a1 100644 --- a/config.lua +++ b/config.lua @@ -5,12 +5,23 @@ web = web --- @type table uv = uv +local function trim(s) return s:gsub("^%s*(.-)%s*$", "%1") end + +local function get_current_tab_index() + local currentTab = web.tabs.current(); + for index, tab in ipairs(web.tabs.list()) do + if tab.id == currentTab then + return index + end + end +end + local Dmenu = {} function Dmenu.select(list, opts, callback) local selection = nil local stdin = uv.new_pipe(); local stdout = uv.new_pipe(); - local args = { '-p', opts.prompt or '>' } + local args = { '-p', opts.prompt or '>', '-it', opts.input or '' } uv.spawn('dmenu', { args = args, stdio = { stdin, stdout, nil } }, function(code) uv.close(stdout) uv.close(stdin) @@ -31,10 +42,6 @@ function Dmenu.select(list, opts, callback) uv.shutdown(stdin) end -local function trim(s) - return s:gsub("^%s*(.-)%s*$", "%1") -end - local urls = { 'https://excalidraw.com', 'https://lite.duckduckgo.com', @@ -57,10 +64,20 @@ web.keymap.set('n', '', function() web.open(trim(result)) end) end) +-- Update current url +web.keymap.set('n', '', function() + local tabs = web.tabs.list() + local tab = tabs[get_current_tab_index()]; + if tab == nil then return end + Dmenu.select(urls, { prompt = 'Set url:', input = tab.url }, function(err, result) + if err or not result then return end + web.open(trim(result)) + end) +end) -- Run lua code web.keymap.set('n', 'q', function() - Dmenu.select({}, { prompt = 'Lua >' }, function(err, result) + Dmenu.select({}, { prompt = 'Lua:' }, function(err, result) if err or not result then return end local run, run_err = load(result) if run_err then print(run_err) end @@ -71,30 +88,27 @@ end) -- History back/forward web.keymap.set('n', '', function() web.history.back(); end) web.keymap.set('n', '', function() web.history.forward(); end) + +-- Close tab web.keymap.set('n', '', function() web.tabs.close(); end) -- Tab select web.keymap.set('n', 'b', function() - local tabsList = {} - for _, tab in ipairs(web.tabs.list()) do - table.insert(tabsList, tab.id .. ': ' .. tab.title .. ' (' .. tab.url) + local tabs_list = {} + local tabs = web.tabs.list() + for index, tab in ipairs(web.tabs.list()) do + table.insert(tabs_list, index .. ': ' .. tab.title .. ' (' .. tab.url .. ')') end - Dmenu.select(tabsList, { prompt = 'Tab:' }, function(err, result) + Dmenu.select(tabs_list, { prompt = 'Tab:' }, function(err, result) if err or not result then return end - local id, _ = trim(result):gsub("%s*:.*$", "") - web.tabs.select(id) + local index_str, _ = trim(result):gsub("%s*:.*$", "") + local index = tonumber(index_str) + if tabs[index] then + web.tabs.select(tabs[index].id) + end end) end) -local function get_current_tab_index() - local currentTab = web.tabs.current(); - for index, tab in ipairs(web.tabs.list()) do - if tab.id == currentTab then - return index - end - end -end - -- Next tab web.keymap.set('n', 'tn', function() local tabs = web.tabs.list() @@ -106,7 +120,6 @@ end) -- Prev tab web.keymap.set('n', 'tp', function() - print('-----------') local tabs = web.tabs.list() if #tabs <= 1 then return end local index = get_current_tab_index() - 1; diff --git a/spec/WebViewStackSpec.cpp b/spec/WebViewStackSpec.cpp index 4324976..aed3748 100644 --- a/spec/WebViewStackSpec.cpp +++ b/spec/WebViewStackSpec.cpp @@ -172,15 +172,15 @@ private slots: // } // } - void test_close_webview() { - context("when closeWebView is called"); - context("- with out of bounds index"); + void test_close() { + context("when close is called"); + context("- with invalid id"); it("does nothing") { Configuration configuration; WebViewStack webview_stack(&configuration); webview_stack.open_url(QUrl("https://a.com"), OpenType::OpenUrl); - webview_stack.close(1); + webview_stack.close(99); QCOMPARE(webview_stack.count(), 1); QCOMPARE(webview_stack.urls(), @@ -189,7 +189,7 @@ private slots: QCOMPARE(webview_stack.current_url(), QUrl("https://a.com")); } - context("when closeWebView is called"); + context("when close is called on current webview"); context("- and there is only 1 tab"); it("closes the tab and opens empty tab in its place") { Configuration configuration; @@ -197,7 +197,7 @@ private slots: webview_stack.open_url(QUrl("https://a.com"), OpenType::OpenUrl); QCOMPARE(webview_stack.count(), 1); - webview_stack.close(0); + webview_stack.close(webview_stack.current_webview_id()); QCOMPARE(webview_stack.count(), 1); QCOMPARE(webview_stack.urls(), @@ -206,8 +206,8 @@ private slots: QCOMPARE(webview_stack.current_url(), configuration.new_tab_url); } - context("when closeWebView is called"); - context("- with the current tab index"); + context("when close is called"); + context("- with the current tab id"); context("- and there are some tabs after"); it("closes the tab and focuses the next tab") { Configuration configuration; @@ -217,7 +217,7 @@ private slots: QCOMPARE(webview_stack.count(), 3); QCOMPARE(webview_stack.current_webview_index(), 1); - webview_stack.close(1); + webview_stack.close(webview_stack.current_webview_id()); QCOMPARE(webview_stack.count(), 2); QCOMPARE(webview_stack.urls(), @@ -227,10 +227,10 @@ private slots: QCOMPARE(webview_stack.current_url(), QUrl("https://a2.com")); } - context("when closeWebView is called"); - context("- with the current tab index"); + context("when close is called"); + context("- with the current tab id"); context("- which is the last tab"); - it("closes the tab and focusses previous tab") { + it("closes the tab and focuses previous tab") { Configuration configuration; WebViewStack webview_stack(&configuration); webview_stack.open_url(QUrl("https://a1.com"), OpenType::OpenUrlInBgTab); @@ -238,7 +238,7 @@ private slots: QCOMPARE(webview_stack.count(), 3); QCOMPARE(webview_stack.current_webview_index(), 2); - webview_stack.close(2); + webview_stack.close(webview_stack.current_webview_id()); QCOMPARE(webview_stack.count(), 2); QCOMPARE(webview_stack.urls(), diff --git a/src/InputMediator.cpp b/src/InputMediator.cpp index 91393c2..175a1b4 100644 --- a/src/InputMediator.cpp +++ b/src/InputMediator.cpp @@ -13,19 +13,22 @@ InputMediator::InputMediator(WebViewStack *webview_stack, KeymapEvaluator *keymap_evaluator) : webview_stack(webview_stack), lua_runtime(lua_runtime), keymap_evaluator(keymap_evaluator) { - connect(lua_runtime, &LuaRuntime::url_opened, webview_stack, - &WebViewStack::open_url); connect(lua_runtime, &LuaRuntime::keymap_added, this, &InputMediator::add_keymap); + connect(lua_runtime, &LuaRuntime::history_back_requested, webview_stack, &WebViewStack::webview_history_back); connect(lua_runtime, &LuaRuntime::history_forward_requested, webview_stack, &WebViewStack::webview_history_forward); + + connect(lua_runtime, &LuaRuntime::url_opened, webview_stack, + &WebViewStack::open_url); connect(lua_runtime, &LuaRuntime::webview_closed, webview_stack, &WebViewStack::close); connect(lua_runtime, &LuaRuntime::webview_selected, webview_stack, &WebViewStack::focus_webview); + // TODO: Think of how to handle this for multi-window lua_runtime->set_current_tab_id_fetcher( [this]() { return this->webview_stack->current_webview_id(); }); lua_runtime->set_webview_data_list_fetcher( diff --git a/src/InputMediator.hpp b/src/InputMediator.hpp index d1dc122..63c7a76 100644 --- a/src/InputMediator.hpp +++ b/src/InputMediator.hpp @@ -16,12 +16,9 @@ public: KeymapEvaluator *keymap_evaluator); ~InputMediator() override; - // DELEGATE(webview_stack, open_url, open_url) - // DELEGATE(webview_stack, current_url, current_url) - // DELEGATE(webview_stack, close_current, close_current_webview) DELEGATE(keymap_evaluator, evaluate, evaluate_keymap) -protected: +protected slots: void add_keymap(const QString &mode_string, const QString &keyseq, std::function action); diff --git a/src/LuaRuntime.cpp b/src/LuaRuntime.cpp index 0b5e698..edf3b74 100644 --- a/src/LuaRuntime.cpp +++ b/src/LuaRuntime.cpp @@ -60,6 +60,60 @@ QVariant LuaRuntime::evaluate_sync(const QString &code) { return get_lua_value(-1); // TODO: error handling } +void LuaRuntime::load_file(const QString &path) { + queue_task([this, path]() { + preserve_top(state, { + if (luaL_dofile(state, path.toStdString().c_str()) != LUA_OK) { + qDebug() << "Load file error:" << lua_tostring(state, -1); + } + }) + }); +} + +void LuaRuntime::init_web_lib() { + // NOLINTBEGIN(modernize-avoid-c-arrays) + + // web + luaL_Reg web[] = { + {"open", &LuaRuntime::lua_open_url}, + {nullptr, nullptr}, + }; + luaL_newlib(state, web); + lua_setglobal(state, web_global_name); + lua_getglobal(state, web_global_name); + + // Keymap api (web.keymap) + luaL_Reg webkeymap[] = { + {"set", &LuaRuntime::lua_keymap_set}, + {nullptr, nullptr}, + }; + luaL_newlib(state, webkeymap); + lua_setfield(state, -2, "keymap"); + + // Tab actions (web.tabs) + luaL_Reg webtabs[] = { + {"close", &LuaRuntime::lua_tab_close}, + {"new", &LuaRuntime::lua_tab_create}, + {"current", &LuaRuntime::lua_tab_current}, + {"list", &LuaRuntime::lua_tab_list}, + {"select", &LuaRuntime::lua_tab_select}, + {nullptr, nullptr}, + }; + luaL_newlib(state, webtabs); + lua_setfield(state, -2, "tabs"); + + // History navigation + luaL_Reg webhistory[] = { + {"back", &LuaRuntime::lua_history_back}, + {"forward", &LuaRuntime::lua_history_forward}, + {nullptr, nullptr}, + }; + luaL_newlib(state, webhistory); + lua_setfield(state, -2, "history"); + + // NOLINTEND(modernize-avoid-c-arrays) +} + QVariant LuaRuntime::get_lua_value(int idx, QVariant default_value) { if (lua_isnoneornil(state, idx)) return default_value; @@ -76,21 +130,21 @@ QVariant LuaRuntime::get_lua_value(int idx, QVariant default_value) { return lua_tostring(state, idx); } -int LuaRuntime::lua_on_url_open(lua_State *state) { +int LuaRuntime::lua_open_url(lua_State *state) { const char *url = luaL_optstring(state, 1, ""); auto *runtime = LuaRuntime::instance(); emit runtime->url_opened(url, OpenType::OpenUrl); return 1; } -int LuaRuntime::lua_on_url_tab_open(lua_State *state) { +int LuaRuntime::lua_tab_create(lua_State *state) { const char *url = luaL_optstring(state, 1, ""); auto *runtime = LuaRuntime::instance(); emit runtime->url_opened(url, OpenType::OpenUrlInTab); return 1; } -int LuaRuntime::lua_add_keymap(lua_State *state) { +int LuaRuntime::lua_keymap_set(lua_State *state) { const char *mode = lua_tostring(state, 1); const char *keyseq = lua_tostring(state, 2); @@ -113,68 +167,14 @@ int LuaRuntime::lua_add_keymap(lua_State *state) { return 1; } -void LuaRuntime::load_file(const QString &path) { - queue_task([this, path]() { - preserve_top(state, { - if (luaL_dofile(state, path.toStdString().c_str()) != LUA_OK) { - qDebug() << "Load file error:" << lua_tostring(state, -1); - } - }) - }); -} - -void LuaRuntime::init_web_lib() { - // NOLINTBEGIN(modernize-avoid-c-arrays) - - // web - luaL_Reg web[] = { - {"open", &LuaRuntime::lua_on_url_open}, - {nullptr, nullptr}, - }; - luaL_newlib(state, web); - lua_setglobal(state, web_global_name); - lua_getglobal(state, web_global_name); - - // Keymap api (web.keymap) - luaL_Reg webkeymap[] = { - {"set", &LuaRuntime::lua_add_keymap}, - {nullptr, nullptr}, - }; - luaL_newlib(state, webkeymap); - lua_setfield(state, -2, "keymap"); - - // Tab actions (web.tabs) - luaL_Reg webtabs[] = { - {"close", &LuaRuntime::lua_tab_closed}, - {"new", &LuaRuntime::lua_on_url_tab_open}, - {"current", &LuaRuntime::lua_get_current_tab_id}, - {"list", &LuaRuntime::lua_get_tab_list}, - {"select", &LuaRuntime::lua_tab_selected}, - {nullptr, nullptr}, - }; - luaL_newlib(state, webtabs); - lua_setfield(state, -2, "tabs"); - - // History navigation - luaL_Reg webhistory[] = { - {"back", &LuaRuntime::lua_history_back}, - {"forward", &LuaRuntime::lua_history_forward}, - {nullptr, nullptr}, - }; - luaL_newlib(state, webhistory); - lua_setfield(state, -2, "history"); - - // NOLINTEND(modernize-avoid-c-arrays) -} - -int LuaRuntime::lua_get_current_tab_id(lua_State *state) { +int LuaRuntime::lua_tab_current(lua_State *state) { auto *runtime = LuaRuntime::instance(); auto tab_id = runtime->fetch_current_tab_id(); lua_pushinteger(state, tab_id); return 1; } -int LuaRuntime::lua_get_tab_list(lua_State *state) { +int LuaRuntime::lua_tab_list(lua_State *state) { auto *runtime = LuaRuntime::instance(); auto tabs = runtime->fetch_webview_data_list(); lua_newtable(state); @@ -235,7 +235,7 @@ int LuaRuntime::lua_history_forward(lua_State *state) { return 1; } -int LuaRuntime::lua_tab_closed(lua_State *state) { +int LuaRuntime::lua_tab_close(lua_State *state) { auto *runtime = LuaRuntime::instance(); WebViewId tab_id; @@ -249,7 +249,7 @@ int LuaRuntime::lua_tab_closed(lua_State *state) { return 1; } -int LuaRuntime::lua_tab_selected(lua_State *state) { +int LuaRuntime::lua_tab_select(lua_State *state) { if (lua_isnoneornil(state, 1)) return 1; // TODO: return nil (for others too) diff --git a/src/LuaRuntime.hpp b/src/LuaRuntime.hpp index 209f8b7..e534d0e 100644 --- a/src/LuaRuntime.hpp +++ b/src/LuaRuntime.hpp @@ -40,29 +40,30 @@ public: DEFINE_FETCHER(QList(), webview_data_list) signals: - void url_opened(QString url, OpenType open_type); void evaluation_completed(QVariant value); void evaluation_failed(QString value); - void keymap_added(QString mode, QString keyseq, std::function); void history_back_requested(WebViewId webview_id, qsizetype history_index); void history_forward_requested(WebViewId webview_id, qsizetype history_index); + void keymap_added(QString mode, QString keyseq, std::function); + void url_opened(QString url, OpenType open_type); void webview_closed(WebViewId webview_id); void webview_selected(WebViewId webview_id); - // void output_produced(QVariant value); protected: LuaRuntime(); ~LuaRuntime() override; void init_web_lib(); - static int lua_on_url_open(lua_State *state); - static int lua_on_url_tab_open(lua_State *state); - static int lua_add_keymap(lua_State *state); - static int lua_get_current_tab_id(lua_State *state); + + // Lua api static int lua_history_back(lua_State *state); static int lua_history_forward(lua_State *state); - static int lua_tab_closed(lua_State *state); - static int lua_get_tab_list(lua_State *state); - static int lua_tab_selected(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); + static int lua_tab_create(lua_State *state); + static int lua_tab_current(lua_State *state); + static int lua_tab_list(lua_State *state); + static int lua_tab_select(lua_State *state); private: lua_State *state; diff --git a/src/main.cpp b/src/main.cpp index 4a30628..d676688 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,4 @@ #include -#include #include "LuaRuntime.hpp" #include "widgets/MainWindow.hpp" diff --git a/src/widgets/MainWindow.cpp b/src/widgets/MainWindow.cpp index c3c392e..d66611b 100644 --- a/src/widgets/MainWindow.cpp +++ b/src/widgets/MainWindow.cpp @@ -45,10 +45,10 @@ MainWindow::MainWindow() { LuaRuntime::instance()->load_file("./config.lua"); // TODO: remove - keymap_evaluator->add_keymap(KeyMode::Normal, "i", [keymap_evaluator]() { + keymap_evaluator->add_keymap(KeyMode::Normal, "i", [&keymap_evaluator]() { keymap_evaluator->set_current_mode(KeyMode::Insert); }); - keymap_evaluator->add_keymap(KeyMode::Insert, "", [keymap_evaluator]() { + keymap_evaluator->add_keymap(KeyMode::Insert, "", [&keymap_evaluator]() { keymap_evaluator->set_current_mode(KeyMode::Normal); }); keymap_evaluator->add_keymap(KeyMode::Normal, "a", diff --git a/src/widgets/WebViewStack.cpp b/src/widgets/WebViewStack.cpp index fda4f28..aee4af9 100644 --- a/src/widgets/WebViewStack.cpp +++ b/src/widgets/WebViewStack.cpp @@ -103,6 +103,7 @@ void WebViewStack::close(WebViewId webview_id) { // TODO: Focus on different webview // focus_webview(); + // TODO: Close window on empty if (webview_list.isEmpty()) { create_new_webview(configuration->new_tab_url, true); } diff --git a/src/widgets/WebViewStack.hpp b/src/widgets/WebViewStack.hpp index b675af5..9df088e 100644 --- a/src/widgets/WebViewStack.hpp +++ b/src/widgets/WebViewStack.hpp @@ -31,12 +31,15 @@ public: QWebEngineProfile *profile = new QWebEngineProfile, QWidget *parent = nullptr); - std::vector urls(); // TODO: Remove QList get_webview_list(); WebView *current_webview(); WebViewId current_webview_id(); uint32_t count(); QUrl current_url(); + + /// @deprecated TODO: Remove + std::vector urls(); + /// @deprecated TODO: Remove uint32_t current_webview_index(); protected: -- cgit v1.3.1