From e6d09718e62c89055d38274e1fa7b42daea6e555 Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Mon, 4 Aug 2025 22:37:49 +0530 Subject: Update hints to go back to normal mode when matching is over --- TODO.org | 39 +++++---- assets/javascript/finder.js | 132 ---------------------------- assets/javascript/hints.js | 152 +++++++++++++++++++++++++++++++++ lua/null-browser/extras/hints.lua | 26 +++--- lua/null-browser/extras/statusline.lua | 2 +- 5 files changed, 190 insertions(+), 161 deletions(-) delete mode 100644 assets/javascript/finder.js create mode 100644 assets/javascript/hints.js diff --git a/TODO.org b/TODO.org index 18bc2ed..16dc417 100644 --- a/TODO.org +++ b/TODO.org @@ -7,49 +7,54 @@ - [X] Run JS in page (web.view.run_js()) - [X] KeyPressed event - [X] f-key navigation -- [ ] On finder stop, exit find mode +- [X] web.view.reload +- [X] Load assets directory into build +- [X] Show number of tabs in statusline +- [X] On finder stop, exit find mode +- [X] Callback for result of run_js +- [ ] Generate docs for api +- [ ] Embed docs in app (`null:/docs`) +- [ ] Update all api to use opts table pattern - [ ] web.decorations.*.set_size() - [ ] 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() +- [ ] Zoom in/out/reset +- [ ] Remove unwanted "async"-ness in lua calls +- [ ] Api for enabling rpc api in view +- [ ] Make the asset/config/lua paths readable via lua ** Bugs -- [ ] Managing focus in decorations? - [ ] INVESTIGATE: Check why urlchanged doesnt fire for first url open sometimes - [ ] INVESTIGATE: Segfault on close sometimes -- [ ] API's don't validate types. (type conversion segfaults) +- [ ] API's don't validate types. (invalid type conversion segfaults) - [ ] web.view apis in `-e` flag from "clients" (non-servers calls) don't work +- [ ] Managing focus in decorations? ** Next -- [ ] Granular updates in statusline plugin instead of set_html multiple times +- [ ] User scripts (greasemonkey?) +- [ ] User stylesheets (per site and global?) +- [ ] Log stdout, errors and results from lua somewhere +- [ ] Move view to a different window (`web.view.move_to_window(view, win)`) +- [ ] web.win.* apis (list, current_id) - [ ] Permission management (list/allow/deny) lua api +- [ ] Granular updates in statusline plugin instead of set_html multiple times +- [ ] Use table for all internals api options? - [ ] Tests for window - [ ] More tests for stack - [ ] Tests for router -- [ ] Read page contents via lua - [ ] User data/profiles management - [ ] Create window with new profile - [ ] Configuration validation -- [ ] web.win.* apis (list, current_id) - [ ] Allow toggling devtools - [X] Set search text as the user is typing (dmenu -r) -- [ ] Log stdout, errors and results from lua somewhere - [ ] Conflict in keymap (keymap already exists) - [ ] Allow pattern filtering for event listeners -- [ ] Allow tab_id, win_id filtering for event listeners +- [ ] Allow view_id, win_id filtering for event listeners - [ ] Handle resource cleanup + signal disconnecting - [ ] Private window (in-memory profile) ** Later - [ ] Bookmarking -- [ ] User scripts (greasemonkey?) -- [ ] User stylesheets (per site and global?) - [ ] Support multiple modes for keymap.set: `web.keymap.set({'n','i'}, ...)` - [ ] static linking for qt - [X] vendor web.inspect with build diff --git a/assets/javascript/finder.js b/assets/javascript/finder.js deleted file mode 100644 index 925a135..0000000 --- a/assets/javascript/finder.js +++ /dev/null @@ -1,132 +0,0 @@ -(() => { - /** @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} - */ - 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} */ - 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/assets/javascript/hints.js b/assets/javascript/hints.js new file mode 100644 index 0000000..ce958f2 --- /dev/null +++ b/assets/javascript/hints.js @@ -0,0 +1,152 @@ +(() => { + /** @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} + */ + 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 hints = { + /** @type {string} */ + keys: '', + /** @type {Array} */ + matches: [], + /** @type {HTMLElement | null} */ + labelsRoot: null, + /** @type {boolean} */ + openInNewView: false, + + stop() { + hints.matches = []; + hints.keys = ''; + [...(hints.labelsRoot?.children ?? [])].forEach(child => { + child.remove(); + }); + }, + + /** + * @param selector {string} + * @param new_view {boolean} + */ + start(selector, new_view) { + hints.stop() + hints.labelsRoot ??= addRoot(); + hints.createMatches(selector); + hints.openInNewView = new_view; + }, + + /** + * @param {string} char + * @return {boolean} + */ + filterOutByKey(char) { + if (!/[0-9]+/.test(char)) return false; + + hints.keys = hints.keys + char; + hints.matches = hints.matches.filter(m => { + const isMatch = m.key.startsWith(hints.keys); + if (!isMatch) { + m.labelElem?.remove(); + return false + } + if (m.labelElem) { + const text = m.labelElem.textContent + const start = text.slice(0, hints.keys.length) + const end = text.slice(hints.keys.length) + m.labelElem.innerHTML = `${start}${end}` + } + return true + }); + + if (hints.matches.length === 0) { + hints.stop() + return true + } + + if (hints.matches.length === 1) { + const match = hints.matches[0] + hints.stop(); + if (!match) { + console.log(hints.matches); + return true; + } + if (match.elem?.href) { + if (hints.openInNewView) { + window.open(match.elem?.href) + } else { + location.href = match.elem?.href + } + } else { + match.elem?.click() + } + return true + } + + return false + }, + + /** @param selector {string} */ + createMatches(selector) { + hints.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) + hints.labelsRoot.appendChild(result.value.labelElem) + hints.matches.push(result.value) + }); + }); + }, + }; + + window._nullbrowser ||= {}; + window._nullbrowser.hints ||= hints; +})(); diff --git a/lua/null-browser/extras/hints.lua b/lua/null-browser/extras/hints.lua index f2ee21b..33d4564 100644 --- a/lua/null-browser/extras/hints.lua +++ b/lua/null-browser/extras/hints.lua @@ -7,15 +7,15 @@ local hints = { local js_setup_code = '' function hints.init(on_ready) - hints.load_finder_js(function() - web.keymap.set(hints.config.mode, '', function() - hints.stop() - end) + web.keymap.set(hints.config.mode, '', function() + hints.stop() + end) + hints._load_hints_js(function() web.event.add_listener('KeyPressed', { callback = function(event) if web.keymap.get_mode() == hints.config.mode then - hints.filter_key(event.key) + hints._filter_key(event.key) end end, }) @@ -28,7 +28,7 @@ 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('" .. + ";_nullbrowser.hints.start('" .. selector .. "', " .. open_in_new_view .. @@ -40,17 +40,21 @@ function hints.start(selector, new_view) end) end -function hints.filter_key(key) - web.view.run_js("_nullbrowser.finder.filterOutByKey('" .. key .. "')") +function hints._filter_key(key) + web.view.run_js("_nullbrowser.hints.filterOutByKey('" .. key .. "')", { + on_result = function(end_of_matches) + if end_of_matches then hints.stop() end + end, + }) end function hints.stop() web.keymap.set_mode('n') - web.view.run_js('_nullbrowser.finder.stop()') + web.view.run_js('_nullbrowser.hints.stop()') end -function hints.load_finder_js(on_ready) - web.uv.fs_open('./assets/javascript/finder.js', 'r', 438, function(err, file) +function hints._load_hints_js(on_ready) + web.uv.fs_open('./assets/javascript/hints.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)) diff --git a/lua/null-browser/extras/statusline.lua b/lua/null-browser/extras/statusline.lua index bcaaea2..b23c3a6 100644 --- a/lua/null-browser/extras/statusline.lua +++ b/lua/null-browser/extras/statusline.lua @@ -9,7 +9,7 @@ local statusline = { }, mode_styles = { n = 'background-color: #007070; color: white;', - i = 'background-color: #e06c75; color: white;', + i = 'background-color: #f04750; color: white;', f = 'background-color: #51e980; color: #333;', }, segments = { -- cgit v1.3.1