aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--TODO.org12
-rw-r--r--assets/javascript/finder.js132
-rw-r--r--init.lua5
-rw-r--r--lua/null-browser/extras/hints.lua65
-rw-r--r--src/events/KeyPressedEvent.hpp25
-rw-r--r--src/widgets/BrowserApp.cpp2
-rw-r--r--src/widgets/BrowserWindow.cpp12
-rw-r--r--src/widgets/BrowserWindow.hpp2
9 files changed, 251 insertions, 5 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 08bf560..4bf1144 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -103,5 +103,6 @@ install(TARGETS ${PROJECT}
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}")
install(DIRECTORY lua/ DESTINATION lua)
+install(DIRECTORY assets/ DESTINATION assets)
# file(COPY "${CMAKE_SOURCE_DIR}/lua" DESTINATION "${CMAKE_INSTALL_PREFIX}")
diff --git a/TODO.org b/TODO.org
index ad7c50b..18bc2ed 100644
--- a/TODO.org
+++ b/TODO.org
@@ -4,13 +4,20 @@
- [X] Create view with html (web.view.set_html())
- [X] Expose JS <-> lua in page (web.view.expose())
- [X] Tests for api
-- [ ] Run JS in page (web.view.eval_js())
+- [X] Run JS in page (web.view.run_js())
+- [X] KeyPressed event
+- [X] f-key navigation
+- [ ] On finder stop, exit find mode
- [ ] web.decorations.*.set_size()
-- [ ] f-key navigation
+- [ ] web.keymap.configure_mode(modename, { passthrough = false })
+- [ ] web.view.reload
+- [ ] Load assets directory into build
- [ ] Fullscreen
- [ ] Zoom in/out
- [ ] Use table for all internals api options?
- [ ] Generate docs for api
+- [ ] Show number of tabs in statusline
+- [ ] web.view.reload()
** Bugs
- [ ] Managing focus in decorations?
@@ -20,6 +27,7 @@
- [ ] web.view apis in `-e` flag from "clients" (non-servers calls) don't work
** Next
+- [ ] Granular updates in statusline plugin instead of set_html multiple times
- [ ] Permission management (list/allow/deny) lua api
- [ ] Tests for window
- [ ] More tests for stack
diff --git a/assets/javascript/finder.js b/assets/javascript/finder.js
new file mode 100644
index 0000000..925a135
--- /dev/null
+++ b/assets/javascript/finder.js
@@ -0,0 +1,132 @@
+(() => {
+ /** @typedef {{ key: string; elem: HTMLElement; labelElem: HTMLElement | null }} Match */
+
+ const addRoot = () => {
+ const root = document.createElement('div');
+ document.body.appendChild(root);
+ return root;
+ }
+
+ /**
+ * @param match {Match}
+ * @return {Promise<HTMLElement | null>}
+ */
+ const createLabelElem = (match) => new Promise((resolve, reject) => {
+ const label = Object.assign(document.createElement('div'), {
+ className: '__nullbrowser_findlabel',
+ textContent: match.key,
+ });
+ requestAnimationFrame(() => {
+ const rect = match.elem.getBoundingClientRect();
+ if (rect.top < 0 | rect.top > window.innerHeight) return reject(null);
+ if (rect.left < 0 | rect.left > window.innerWidth) return reject(null);
+
+ resolve(label);
+ requestAnimationFrame(() => {
+ Object.assign(label.style, {
+ position: 'fixed',
+ zIndex: 999999999, // maybe a few more 9s will do
+ top: `${rect.top}px`,
+ left: `${rect.left}px`,
+ backgroundColor: 'yellow',
+ border: '1px solid #000',
+ boxShadow: '0 0 3px #555',
+ color: '#222',
+ padding: '1px 2px',
+ fontSize: '10px',
+ fontWeight: 'bold',
+ fontFamily: 'monospace',
+ });
+ });
+ });
+ });
+
+ const finder = {
+ /** @type {string} */
+ keys: '',
+ /** @type {Array<Match>} */
+ matches: [],
+ /** @type {HTMLElement | null} */
+ labelsRoot: null,
+ /** @type {boolean} */
+ openInNewView: false,
+
+ stop() {
+ finder.matches = [];
+ finder.keys = '';
+ [...(finder.labelsRoot?.children ?? [])].forEach(child => {
+ child.remove();
+ });
+ },
+
+ /**
+ * @param selector {string}
+ * @param new_view {boolean}
+ */
+ start(selector, new_view) {
+ finder.stop()
+ finder.labelsRoot ??= addRoot();
+ finder.createMatches(selector);
+ finder.openInNewView = new_view;
+ },
+
+ /** @param char {string} */
+ filterOutByKey(char) {
+ if (!/[0-9]+/.test(char)) return; // NOTE: temporary
+
+ finder.keys = finder.keys + char;
+ finder.matches = finder.matches.filter(m => {
+ const isMatch = m.key.startsWith(finder.keys);
+ if (!isMatch) {
+ m.labelElem?.remove();
+ return false;
+ }
+ return true;
+ });
+ if (finder.matches.length <= 1) {
+ const match = finder.matches[0]
+ finder.stop();
+ if (match) {
+ if (match.elem?.href) {
+ if (finder.openInNewView) {
+ window.open(match.elem?.href)
+ } else {
+ location.href = match.elem?.href
+ }
+ } else {
+ match.elem?.click()
+ }
+ }
+ }
+ },
+
+ /** @param selector {string} */
+ createMatches(selector) {
+ finder.matches = [];
+ const elements = document.querySelectorAll(selector);
+
+ const matches = [...elements].map(async (elem, index) => {
+ /** @type {Match} */
+ const match = {
+ elem,
+ key: `${index}`.padStart(`${elements.length}`.length, '0'),
+ labelElem: null,
+ }
+ match.labelElem = await createLabelElem(match);
+ return match
+ });
+
+ Promise.allSettled(matches).then(matches => {
+ matches.forEach(result => {
+ if (result.status !== 'fulfilled') return;
+ if (result.value.labelElem)
+ finder.labelsRoot.appendChild(result.value.labelElem)
+ finder.matches.push(result.value)
+ });
+ });
+ },
+ };
+
+ window._nullbrowser ||= {};
+ window._nullbrowser.finder ||= finder;
+})();
diff --git a/init.lua b/init.lua
index ca48c7a..f351ca8 100644
--- a/init.lua
+++ b/init.lua
@@ -62,4 +62,9 @@ require 'null-browser.extras.statusline'.init {
decoration = web.decorations.bottom,
}
+local hints = require 'null-browser.extras.hints'
+hints.init()
+web.keymap.set('n', 'f', function() hints.start('a[href], button', false) end)
+web.keymap.set('n', '<s-f>', function() hints.start('a[href], button', true) end)
+
print('ending...')
diff --git a/lua/null-browser/extras/hints.lua b/lua/null-browser/extras/hints.lua
new file mode 100644
index 0000000..f2ee21b
--- /dev/null
+++ b/lua/null-browser/extras/hints.lua
@@ -0,0 +1,65 @@
+local hints = {
+ config = {
+ mode = 'f',
+ },
+}
+
+local js_setup_code = ''
+
+function hints.init(on_ready)
+ hints.load_finder_js(function()
+ web.keymap.set(hints.config.mode, '<Esc>', function()
+ hints.stop()
+ end)
+
+ web.event.add_listener('KeyPressed', {
+ callback = function(event)
+ if web.keymap.get_mode() == hints.config.mode then
+ hints.filter_key(event.key)
+ end
+ end,
+ })
+
+ if type(on_ready) == 'function' then on_ready() end
+ end)
+end
+
+function hints.start(selector, new_view)
+ local open_in_new_view = new_view and 'true' or 'false'
+ web.view.run_js(
+ js_setup_code ..
+ ";_nullbrowser.finder.start('" ..
+ selector ..
+ "', " ..
+ open_in_new_view ..
+ ")"
+ )
+ web.schedule(function()
+ -- Trigger f mode after tick to avoid the trigger key (f) to get captured in event
+ web.keymap.set_mode(hints.config.mode)
+ end)
+end
+
+function hints.filter_key(key)
+ web.view.run_js("_nullbrowser.finder.filterOutByKey('" .. key .. "')")
+end
+
+function hints.stop()
+ web.keymap.set_mode('n')
+ web.view.run_js('_nullbrowser.finder.stop()')
+end
+
+function hints.load_finder_js(on_ready)
+ web.uv.fs_open('./assets/javascript/finder.js', 'r', 438, function(err, file)
+ if err then return end
+ if not file then return {} end
+ local stat = assert(web.uv.fs_fstat(file))
+ local js = assert(web.uv.fs_read(file, stat.size))
+ assert(web.uv.fs_close(file))
+ js_setup_code = js
+
+ on_ready()
+ end)
+end
+
+return hints
diff --git a/src/events/KeyPressedEvent.hpp b/src/events/KeyPressedEvent.hpp
new file mode 100644
index 0000000..8c7ded1
--- /dev/null
+++ b/src/events/KeyPressedEvent.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <QtCore>
+#include <lua.hpp>
+#include <qevent.h>
+
+#include "events/Event.hpp"
+
+class KeyPressedEvent : public Event {
+public:
+ const QString key;
+
+ KeyPressedEvent(QString key) : key(std::move(key)) { kind = "KeyPressed"; }
+
+ static KeyPressedEvent *from_qkeyevent(QKeyEvent *qevent) {
+ // TODO: Using with modifiers
+ auto *event = new KeyPressedEvent(qevent->text());
+ return event;
+ }
+
+ void lua_push(lua_State *state) const override {
+ Event::lua_push(state);
+ SET_FIELD("key", string, key.toStdString().c_str())
+ }
+};
diff --git a/src/widgets/BrowserApp.cpp b/src/widgets/BrowserApp.cpp
index d6f274b..c3c2313 100644
--- a/src/widgets/BrowserApp.cpp
+++ b/src/widgets/BrowserApp.cpp
@@ -84,7 +84,7 @@ bool BrowserApp::eventFilter(QObject *target, QEvent *event) {
if (auto *target_widget = dynamic_cast<QWidget *>(target); win->isAncestorOf(target_widget)) {
auto *key_event = static_cast<QKeyEvent *>(event);
- const bool should_skip = win->on_window_key_event(key_event);
+ const bool should_skip = win->on_window_key_event(target, key_event);
return should_skip;
}
}
diff --git a/src/widgets/BrowserWindow.cpp b/src/widgets/BrowserWindow.cpp
index 1971b9c..7852a66 100644
--- a/src/widgets/BrowserWindow.cpp
+++ b/src/widgets/BrowserWindow.cpp
@@ -4,9 +4,13 @@
#include <QStackedLayout>
#include <QVBoxLayout>
#include <QtCore>
+#include <qcoreevent.h>
#include "Configuration.hpp"
+#include "WindowActionRouter.hpp"
+#include "events/KeyPressedEvent.hpp"
#include "keymap/KeymapEvaluator.hpp"
+#include "widgets/BrowserApp.hpp"
#include "widgets/Decorations.hpp"
#include "widgets/WebViewStack.hpp"
@@ -52,10 +56,16 @@ BrowserWindow::BrowserWindow(const Configuration &configuration, QWebEngineProfi
&BrowserWindow::close_window_requested);
}
-bool BrowserWindow::on_window_key_event(QKeyEvent *event) {
+bool BrowserWindow::on_window_key_event(QObject *target, QKeyEvent *event) {
auto &keymap_evaluator = KeymapEvaluator::instance();
const bool should_skip = keymap_evaluator.evaluate(event->modifiers(), (Qt::Key)event->key());
+ // TODO: Fix this logic to find the right "target" for event
+ if (event->type() == QEvent::KeyPress && dynamic_cast<WebView *>(target->parent())) {
+ auto *lua_event = KeyPressedEvent::from_qkeyevent(event);
+ WindowActionRouter::instance().dispatch_event(lua_event);
+ }
+
return should_skip;
}
diff --git a/src/widgets/BrowserWindow.hpp b/src/widgets/BrowserWindow.hpp
index 2ea7b88..bc6cea1 100644
--- a/src/widgets/BrowserWindow.hpp
+++ b/src/widgets/BrowserWindow.hpp
@@ -41,7 +41,7 @@ public:
void run_javascript(const QString &js_code, WebViewId webview_id);
void expose_rpc_function(const QString &name, const RpcFunc &action, WebViewId webview_id);
- bool on_window_key_event(QKeyEvent *event);
+ bool on_window_key_event(QObject *target, QKeyEvent *event);
void closeEvent(QCloseEvent * /*event*/) override { emit closed(); };