agenda: a card's date and list without going to its board

In the agenda, `D` asks for the highlighted card's due date on the command
line (`:due` now works there as well as on the board), and `H` / `L` send
the card to the list left or right of the one it is in, on its own board.
`L` into the last list is done, as on the board.

The command line now rises above an open overlay, and while it is up the
overlay's own keys stay out of its way, so typing a date does not walk the
agenda.
This commit is contained in:
nvrl committed 2026-10-20 15:34:07 +02:00
1 parent cb76aaf43a
commit 811e09416f
7 files changed
+64 -13

No files matched your search

+2 -1
View File
@@ -230,7 +230,8 @@ file is refused.
due within the week, or sitting in a column between the inbox and done — the
work already in hand — and says whether the diary has an entry for today. `j`/`k`
walk it, `Enter` goes to the card on its own board, `Space` finishes it where
it stands, and `T` writes today's entry. The bar shows `late` and `today` counts
it stands, `D` sets its due date (`:due` on the line), `H` / `L` send it to
the list left / right on its board, and `T` writes today's entry. The bar shows `late` and `today` counts
whenever either is above zero, and the agenda opens by itself once a day when
something is late or due, so nothing has to be remembered to be looked at.
+18 -5
View File
@@ -307,6 +307,17 @@
case ' ':
if (card) void workspace.guard(() => workspace.finishCard(card))
return true
case 'D':
// The agenda stays up behind the line, so `:due` knows which card.
if (card) {
cmdGlobal = false
cmdline = ':due '
}
return true
case 'H':
case 'L':
if (card) void workspace.guard(() => workspace.sendAgendaCard(card, event.key === 'H' ? -1 : 1))
return true
case 'T':
workspace.overlay = 'none'
void runCommand('today')
@@ -526,25 +537,27 @@
return
}
if (workspace.overlay === 'overview' && !event.ctrlKey && !event.metaKey && !event.altKey) {
// The command line over an overlay — `D` in the agenda — keeps its keys.
const listening = cmdline === null && !isTypingTarget(event.target)
if (workspace.overlay === 'overview' && listening && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (overviewKey(event)) {
event.preventDefault()
return
}
}
if (workspace.overlay === 'agenda' && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (workspace.overlay === 'agenda' && listening && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (agendaKey(event)) {
event.preventDefault()
return
}
}
if (workspace.overlay === 'media' && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (workspace.overlay === 'media' && listening && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (mediaKey(event)) {
event.preventDefault()
return
}
}
if (workspace.overlay === 'outline' && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (workspace.overlay === 'outline' && listening && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (outlineKey(event)) {
event.preventDefault()
return
@@ -567,7 +580,7 @@
}
}
if (workspace.overlay !== 'none') {
if (workspace.overlay !== 'none' && listening) {
if (event.key === 'q' || event.key === '?') {
event.preventDefault()
workspace.overlay = 'none'
+4
View File
@@ -87,6 +87,10 @@
<span class="gap"></span>
<span class="kbd">␣</span> done
<span class="gap"></span>
<span class="kbd">D</span> due
<span class="gap"></span>
<span class="kbd">H</span><span class="kbd">L</span> list
<span class="gap"></span>
<span class="kbd">T</span> diary
<span class="gap"></span>
<span class="kbd">t</span><span class="kbd">esc</span> close
+2 -1
View File
@@ -241,7 +241,8 @@
left: calc(var(--frame) + 12px);
right: calc(var(--rail-w) + 12px);
bottom: calc(var(--status-h) - 1px);
z-index: 20;
/* Above the overlays, so `D` in the agenda can ask for a date. */
z-index: 35;
display: flex;
flex-direction: column;
background: color-mix(in oklab, var(--hull) 97%, transparent);
+2
View File
@@ -70,6 +70,8 @@
['j / k', 'down / up the cards'],
['Enter', 'to the card, on its own board'],
['Space', 'done, where it stands'],
['D', 'due date: fri, tm, +3d … — :due on the line'],
['H / L', 'to the list left / right on its board'],
['T', "today's diary entry"],
['t · q · Esc', 'close the agenda'],
]
+7 -6
View File
@@ -425,17 +425,18 @@ export const commands: CommandSpec[] = [
name: 'due',
args: '<when|->',
group: 'board',
help: 'due date of the card: today, tm, fri, +3d, +2w, 10-31, 2026-10-31 — - clears',
help: 'due date of the card — on the board, or highlighted in the agenda: today, tm, fri, +3d, +2w, 10-31, 2026-10-31 — - clears',
complete: () => ['today', 'tm', 'fri', '+3d'],
run: async (arg) => {
if (workspace.view !== 'board') return workspace.say('no board open — :board', 'error')
const agenda = workspace.overlay === 'agenda'
if (workspace.view !== 'board' && !agenda) return workspace.say('no board open — :board', 'error')
const set = (due_at: number | null) =>
agenda ? workspace.dueAgendaCard(due_at) : workspace.patchCard({ due_at })
const target = arg.trim()
if (target === '-' || target === '') {
return workspace.guard(() => workspace.patchCard({ due_at: null }))
}
if (target === '-' || target === '') return workspace.guard(() => set(null))
const at = parseDue(target)
if (at === null) return workspace.say(`not a date: ${target} — try fri, +3d or 2026-09-30`, 'error')
await workspace.guard(() => workspace.patchCard({ due_at: at }))
await workspace.guard(() => set(at))
workspace.say(`due ${formatDay(at)}`)
},
},
+29
View File
@@ -1160,6 +1160,35 @@ class Workspace {
this.say(`done: ${card.title}`)
}
/** `:due` with the agenda up: the highlighted card's date, on its own board. */
async dueAgendaCard(due_at: number | null) {
const card = this.agendaCards[this.agendaCursor]
if (!card) return this.say('no card highlighted', 'error')
await api.updateCard(card.id, { due_at })
await this.loadAgenda()
// It may have changed group; the cursor goes with it.
const at = this.agendaCards.findIndex((c) => c.id === card.id)
if (at >= 0) this.agendaCursor = at
if (this.view === 'board' && this.spaceId === card.space_id) await this.reloadBoard()
}
/**
* `H` / `L` in the agenda: the card to the list left or right of the one it
* is in, without going to the board — `L` on the last list but one is done.
*/
async sendAgendaCard(card: AgendaCard, delta: number) {
const lists = (await api.board(card.space_id)).lists
const from = lists.findIndex((list) => list.id === card.list_id)
const target = lists[from + delta]
if (from < 0 || !target) return this.say(`${card.title} is already in the ${delta < 0 ? 'first' : 'last'} list`, 'error')
await api.moveCard(card.id, target.id, target.cards.length)
await this.loadAgenda()
const at = this.agendaCards.findIndex((c) => c.id === card.id)
if (at >= 0) this.agendaCursor = at
if (this.view === 'board' && this.spaceId === card.space_id) await this.reloadBoard()
this.say(from + delta === lists.length - 1 ? `done: ${card.title}` : `"${card.title}" → ${target.name}`)
}
async loadOverview() {
this.overview = await api.overview()
}