focus: keys go where the screen says they go

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.
This commit is contained in:
nvrl committed 2026-10-20 13:40:40 +02:00
1 parent e0a4c01654
commit 4bf8634991
3 files changed
+44 -2

No files matched your search

+9
View File
@@ -443,6 +443,15 @@
if (cmdline !== null || isTypingTarget(event.target)) return 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 // Ctrl-P: find anything, in any space — the one search that is not
// confined to where you are. // confined to where you are.
if (event.ctrlKey && !event.metaKey && !event.altKey && event.key === 'p') { if (event.ctrlKey && !event.metaKey && !event.altKey && event.key === 'p') {
+19
View File
@@ -10,15 +10,34 @@
* field alone: it is not the login, and their menus redrawn on every key * field alone: it is not the login, and their menus redrawn on every key
* are a flicker of their own. * are a flicker of their own.
*/ */
import { untrack } from 'svelte'
import { hooks } from './commands'
import { workspace } from './store.svelte' import { workspace } from './store.svelte'
let value = $state('') let value = $state('')
let input = $state<HTMLInputElement>() let input = $state<HTMLInputElement>()
/*
* 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(() => { $effect(() => {
if (workspace.secret) { if (workspace.secret) {
value = '' value = ''
if (!asking) returnTo = document.activeElement as HTMLElement | null
asking = true
input?.focus() 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
} }
}) })
+16 -2
View File
@@ -276,6 +276,8 @@ class Workspace {
if (this.vault.unlocked) return true if (this.vault.unlocked) return true
const password = await this.askSecret('vault password') const password = await this.askSecret('vault password')
if (password === null) return false 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.vault = await api.unlockVault(password)
this.say('vault unlocked') this.say('vault unlocked')
return true return true
@@ -583,7 +585,8 @@ class Workspace {
} }
if (spot && this.parentId === spot.parentId) { if (spot && this.parentId === spot.parentId) {
if (spot.openId !== null && this.nodes.some((n) => n.id === spot.openId)) { 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 this.pane = spot.pane
} else { } else {
this.cursor = Math.min(spot.cursor, Math.max(this.nodes.length - 1, 0)) 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) { 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.query = query
this.globalSearch = global && query !== '' this.globalSearch = global && query !== ''
this.cursor = 0 this.cursor = 0
@@ -648,10 +653,19 @@ class Workspace {
this.cursor = Math.min(Math.max(this.cursor + delta, 0), this.nodes.length - 1) 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() if (this.open?.id !== id) await this.flush()
const node = await api.getNode(id) 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)) { if (isContainer(node)) {
await this.goTo(node.id) await this.goTo(node.id)
return return