keys: files and uploads answer the keyboard

`gx` opens the file under the cursor in a tab of its own, as the browser
shows it. `:download` (`:dl`) downloads it, after asking for the vault when
the file is sealed; on the open document it is `:export`. `:source` turns a
markdown file between rendered and as written, which before had only a
button. A sealed file asks for the vault before `gx` instead of after, so
the browser does not block the late tab as a popup.

The media list gets a cursor: `j`/`k` walk it, `i` or `Enter` embeds the
highlighted upload at the cursor while writing, `y` copies its address and
`x` deletes it. Deleting asks through the `[y/N]` prompt, which names what
would break, in place of the browser's confirm(): that one was the last
modal on the page and needed the pointer to dismiss. A footer lists the
keys, as the agenda's does.
This commit is contained in:
nvrl committed 2026-10-20 13:59:09 +02:00
1 parent 54bc333be2
commit 37443cf083
7 files changed
+172 -28

No files matched your search

+32 -1
View File
@@ -24,6 +24,9 @@ export const hooks = {
hintRoot: (): HTMLElement | null => null,
/* Filled in by the file pane: what Enter does there, false when nothing. */
fileAction: (): boolean => false,
/* `:source`: a markdown file between rendered and raw; false when the pane
shows no markdown. */
toggleSource: (): boolean => false,
/* `]]` / `[[` in whatever is being read; false when it has no headings. */
jumpHeading: (_direction: 1 | -1): boolean => false,
/* `gO`: the headings of what is on screen, read or being written, and the
@@ -760,6 +763,33 @@ export const commands: CommandSpec[] = [
},
},
},
{
name: 'download',
aliases: ['dl'],
group: 'sharing',
help: 'download the file under the cursor, or the open one — the open document goes as :export writes it',
run: () =>
workspace.guard(async () => {
const node = workspace.target
if (!node) return workspace.say('nothing selected', 'error')
if (node.kind === 'document' && node.id === workspace.open?.id) return runCommand('export')
if (!node.file) return workspace.say(`a ${node.kind} is no file — :export! downloads everything`, 'error')
if (node.file.encrypted && !(await workspace.ensureUnlocked())) return
const anchor = document.createElement('a')
anchor.href = node.file.url
anchor.download = node.name
anchor.click()
workspace.say(`downloading ${node.name}`)
}),
},
{
name: 'source',
group: 'documents',
help: 'show an open markdown file as written, or rendered again',
run: () => {
if (!hooks.toggleSource()) workspace.say('only an open markdown file has a source to show', 'error')
},
},
{
name: 'upload',
aliases: ['up'],
@@ -777,9 +807,10 @@ export const commands: CommandSpec[] = [
{
name: 'media',
group: 'editing',
help: 'browse everything you have uploaded',
help: 'browse everything you have uploaded — j/k, i inserts it while writing, y copies its address, x deletes',
run: () => workspace.guard(async () => {
await workspace.loadMedia()
workspace.mediaCursor = 0
workspace.overlay = 'media'
}),
},