keys: links are followed without the pointer

`F` puts a letter or two over every link and button in view, vimium-style,
and typing one does what a click on it would. `f` was already fullscreen, so
the hints live on the other key. The labels start from the home row and are
all one length, so a label is done the moment it is typed. Esc, a key that
matches no label, or anything that scrolls the page puts them away. In the
file pane they reach the unlock and keystore buttons too.

A workspace link (`/n/…`) opens in place, as a click on it already did. Any
other link gets a tab of its own instead of taking the workspace with it.

In the editor, `gf` follows the link under the cursor, or the next one on
the line. A workspace link saves the text and opens that document. `gx`
opens the link in a new tab, as vim hands it to the browser. Both read
markdown links, embeds, `<…>` autolinks and bare addresses.
This commit is contained in:
nvrl committed 2026-10-20 13:55:48 +02:00
1 parent 6a8ffdf408
commit 54bc333be2
9 files changed
+226 -6

No files matched your search

+15
View File
@@ -9,6 +9,7 @@
import FilePane from './lib/FilePane.svelte'
import Headings from './lib/Headings.svelte'
import Help from './lib/Help.svelte'
import Hints from './lib/Hints.svelte'
import Verify from './lib/Verify.svelte'
import Login from './lib/Login.svelte'
import Overview from './lib/Overview.svelte'
@@ -44,6 +45,8 @@
let shell = $state<HTMLDivElement>()
let overlayEl = $state<HTMLDivElement>()
let pending = $state('')
/** `F`: the pane whose links and buttons are wearing hints right now. */
let hintRoot = $state<HTMLElement | null>(null)
if (!shareToken) void workspace.boot()
@@ -625,6 +628,14 @@
if (workspace.open && workspace.selected?.id === workspace.open.id) focusPane('document')
else void workspace.guard(() => workspace.openSelected())
break
case 'F': {
// `f` already fills the screen, so the hints vimium puts on `f` live
// on its other key.
event.preventDefault()
const root = reading ? hooks.hintRoot() : null
if (root) hintRoot = root
break
}
case 'm':
// Pick nodes up here, walk to a folder, put them down with `p`.
event.preventDefault()
@@ -809,6 +820,10 @@
<Prompt />
<SecretPrompt />
{#if hintRoot}
<Hints root={hintRoot} close={() => (hintRoot = null)} />
{/if}
{#if workspace.overlay !== 'none'}
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
<div class="overlay" tabindex="-1" bind:this={overlayEl} onclick={(e) => e.target === e.currentTarget && (workspace.overlay = 'none')}