diff --git a/README.md b/README.md
index ef14666..111e790 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/web/src/App.svelte b/web/src/App.svelte
index bc7d2ff..90160ca 100644
--- a/web/src/App.svelte
+++ b/web/src/App.svelte
@@ -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'
diff --git a/web/src/lib/Agenda.svelte b/web/src/lib/Agenda.svelte
index 57efc46..64e9b5c 100644
--- a/web/src/lib/Agenda.svelte
+++ b/web/src/lib/Agenda.svelte
@@ -87,6 +87,10 @@
␣ done
+ D due
+
+ HL list
+
T diary
tesc close
diff --git a/web/src/lib/CommandLine.svelte b/web/src/lib/CommandLine.svelte
index 6be78e4..551f939 100644
--- a/web/src/lib/CommandLine.svelte
+++ b/web/src/lib/CommandLine.svelte
@@ -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);
diff --git a/web/src/lib/Help.svelte b/web/src/lib/Help.svelte
index 88bde39..c4e31ac 100644
--- a/web/src/lib/Help.svelte
+++ b/web/src/lib/Help.svelte
@@ -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'],
]
diff --git a/web/src/lib/commands.ts b/web/src/lib/commands.ts
index 4c65203..f3abe9c 100644
--- a/web/src/lib/commands.ts
+++ b/web/src/lib/commands.ts
@@ -425,17 +425,18 @@ export const commands: CommandSpec[] = [
name: 'due',
args: '',
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)}`)
},
},
diff --git a/web/src/lib/store.svelte.ts b/web/src/lib/store.svelte.ts
index 796641f..b18d53c 100644
--- a/web/src/lib/store.svelte.ts
+++ b/web/src/lib/store.svelte.ts
@@ -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()
}