From 4bf86349912d1cec63bb6990975294c9c55025e3 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 20 Oct 2026 13:40:40 +0200 Subject: [PATCH] focus: keys go where the screen says they go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A password prompt now hands the focus back to whatever had it before it opened, or to the editor or the list when that is gone. Until now the focus was left on nothing once the field went away, so after an :unlock typed in the editor the status line still said NORMAL while j walked the list and dd asked to delete the document. A key that lands outside the editor while it is open is handed back to it for the same reason. A document or folder asked for from the board — T, :12, :e, a search — is now shown instead of being opened in a tree hidden behind the board, and closing such a document goes back to the board. A space switch that reopens a remembered document still keeps the board on screen. Unlocking the vault says so while the key is stretched. --- web/src/App.svelte | 9 +++++++++ web/src/lib/SecretPrompt.svelte | 19 +++++++++++++++++++ web/src/lib/store.svelte.ts | 18 ++++++++++++++++-- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/web/src/App.svelte b/web/src/App.svelte index a5eae15..b86416f 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -443,6 +443,15 @@ if (cmdline !== null || isTypingTarget(event.target)) return + // While writing, the status line says the editor is listening. A key that + // lands anywhere else — the focus left behind by a prompt, a click on the + // header — hands the focus back to it instead of walking the list unseen. + if (workspace.editing && workspace.pane === 'document' && !event.ctrlKey && !event.metaKey && !event.altKey) { + event.preventDefault() + hooks.focusEditor() + return + } + // Ctrl-P: find anything, in any space — the one search that is not // confined to where you are. if (event.ctrlKey && !event.metaKey && !event.altKey && event.key === 'p') { diff --git a/web/src/lib/SecretPrompt.svelte b/web/src/lib/SecretPrompt.svelte index 3423951..ed53e9f 100644 --- a/web/src/lib/SecretPrompt.svelte +++ b/web/src/lib/SecretPrompt.svelte @@ -10,15 +10,34 @@ * field alone: it is not the login, and their menus redrawn on every key * are a flicker of their own. */ + import { untrack } from 'svelte' + import { hooks } from './commands' import { workspace } from './store.svelte' let value = $state('') let input = $state() + /* + * Where the keys went before the question took them. Putting the field away + * would otherwise leave the focus nowhere, and the next keys — meant for the + * editor the status line still says is listening — would walk the list. + */ + let returnTo: HTMLElement | null = null + let asking = false $effect(() => { if (workspace.secret) { value = '' + if (!asking) returnTo = document.activeElement as HTMLElement | null + asking = true input?.focus() + } else if (asking) { + asking = false + untrack(() => { + if (returnTo?.isConnected && returnTo !== document.body) returnTo.focus() + else if (workspace.editing && workspace.pane === 'document') hooks.focusEditor() + else hooks.focusList() + }) + returnTo = null } }) diff --git a/web/src/lib/store.svelte.ts b/web/src/lib/store.svelte.ts index 2d7c7de..ffdcfa3 100644 --- a/web/src/lib/store.svelte.ts +++ b/web/src/lib/store.svelte.ts @@ -276,6 +276,8 @@ class Workspace { if (this.vault.unlocked) return true const password = await this.askSecret('vault password') if (password === null) return false + // The key is stretched on purpose, which takes a moment worth saying. + this.say('unlocking …') this.vault = await api.unlockVault(password) this.say('vault unlocked') return true @@ -583,7 +585,8 @@ class Workspace { } if (spot && this.parentId === spot.parentId) { if (spot.openId !== null && this.nodes.some((n) => n.id === spot.openId)) { - await this.openNode(spot.openId) + // Reopened under the board, if that is what is on screen. + await this.openNode(spot.openId, false, true) this.pane = spot.pane } else { this.cursor = Math.min(spot.cursor, Math.max(this.nodes.length - 1, 0)) @@ -637,6 +640,8 @@ class Workspace { } async search(query: string, global = this.globalSearch) { + // The results are rows in the list, which the board would hide. + if (query && this.view === 'board') this.closeBoard() this.query = query this.globalSearch = global && query !== '' this.cursor = 0 @@ -648,10 +653,19 @@ class Workspace { this.cursor = Math.min(Math.max(this.cursor + delta, 0), this.nodes.length - 1) } - async openNode(id: number, edit = false) { + async openNode(id: number, edit = false, underBoard = false) { if (this.open?.id !== id) await this.flush() const node = await api.getNode(id) + // What is asked for from the board — `:12`, `T`, a search — is shown, not + // opened behind it; closing a document goes back to the board. + if (this.view === 'board' && !underBoard) { + if (isContainer(node)) this.closeBoard() + else { + this.view = 'tree' + this.returnTo = 'board' + } + } if (isContainer(node)) { await this.goTo(node.id) return