diary: templates, a calendar, the days either side and this day before

A folder called templates at the root of any space holds templates. `:new
<template> [yyyy-mm-dd] [name]` starts a document from one, with {{date}},
{{weekday}}, {{time}} and {{title}} filled in on the server, in
WORKSPACE_TIMEZONE, for the date the document is for. A new diary entry starts
from templates/day when the diary has one, whether it comes from T, o, the
calendar or a capture. Templates are not entries: the calendar, the reminder
and "written today" leave them out.

C (or :cal) shows the diary by month: h/l a day, j/k a week, H/L a month, .
today, and Enter opens the day's entry or starts one dated that day. [d and
]d step to the entry before or after the open one by date, backdated ones
included. The agenda ends with what the diary holds from this day in earlier
years; a 29 February is remembered on the 28th in years without one.

New routes: GET /api/diary/days?month=, GET /api/diary/around/{id} and
GET /api/nodes/{id}/templates. POST /api/nodes takes template_id.
This commit is contained in:
nvrl committed 2026-10-20 23:41:04 +02:00
1 parent f629f5af97
commit 824f2bb945
14 files changed
+1048 -50

No files matched your search

+59 -4
View File
@@ -2,6 +2,7 @@
import { cycleTile, focusTile, followLink, hooks, runCommand } from './lib/commands'
import { embedSnippet } from './lib/markdown'
import Agenda from './lib/Agenda.svelte'
import Calendar from './lib/Calendar.svelte'
import Bar from './lib/Bar.svelte'
import Board from './lib/Board.svelte'
import CommandLine from './lib/CommandLine.svelte'
@@ -25,7 +26,7 @@
import Sidebar from './lib/Sidebar.svelte'
import StatusBar from './lib/StatusBar.svelte'
import { workspace } from './lib/store.svelte'
import { dwindle, neighbour, type Direction } from './lib/util'
import { dwindle, formatDay, neighbour, type Direction } from './lib/util'
/*
* A share link is /s/<token>#<key>. The fragment never leaves the browser, so
@@ -292,14 +293,18 @@
return false
}
/** The agenda's keys: j/k down the list, Enter to the card, Space finishes it. */
/**
* The agenda's keys: j/k down the list, Enter to the card, Space finishes it.
* Past the cards the cursor walks on into what was written on this day.
*/
function agendaKey(event: KeyboardEvent): boolean {
const cards = workspace.agendaCards
const card = cards[workspace.agendaCursor]
const past = workspace.agenda.on_this_day[workspace.agendaCursor - cards.length]
switch (event.key) {
case 'j':
case 'ArrowDown':
workspace.agendaCursor = Math.min(workspace.agendaCursor + 1, Math.max(cards.length - 1, 0))
workspace.agendaCursor = Math.min(workspace.agendaCursor + 1, Math.max(cards.length + workspace.agenda.on_this_day.length - 1, 0))
return true
case 'k':
case 'ArrowUp':
@@ -307,6 +312,10 @@
return true
case 'Enter':
if (card) void workspace.guard(() => workspace.goToCard(card))
else if (past) {
workspace.overlay = 'none'
void workspace.guard(() => workspace.openPath(past.path))
}
return true
case ' ':
if (card) void workspace.guard(() => workspace.finishCard(card))
@@ -335,6 +344,35 @@
}
}
/** The calendar's keys: h/l a day, j/k a week, H/L a month, Enter to the day. */
function calendarKey(event: KeyboardEvent): boolean {
const step: Record<string, [number, number]> = {
h: [-1, 0], ArrowLeft: [-1, 0], l: [1, 0], ArrowRight: [1, 0],
k: [-7, 0], ArrowUp: [-7, 0], j: [7, 0], ArrowDown: [7, 0],
H: [0, -1], L: [0, 1],
}
if (event.key in step) {
const [days, months] = step[event.key]
void workspace.guard(() => workspace.moveCalendar(days, months))
return true
}
switch (event.key) {
case '.':
workspace.calendarDay = formatDay(Math.floor(Date.now() / 1000))
void workspace.guard(() => workspace.loadCalendar())
return true
case 'Enter':
void workspace.guard(() => workspace.openCalendarDay()).then(() => hooks.focusEditor())
return true
case 'C':
case 'q':
workspace.overlay = 'none'
return true
default:
return false
}
}
/** `gO`'s list: j/k down the headings, Enter to the one picked. */
function outlineKey(event: KeyboardEvent): boolean {
const last = Math.max(workspace.outline.length - 1, 0)
@@ -675,6 +713,12 @@
return
}
}
if (workspace.overlay === 'calendar' && listening && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (calendarKey(event)) {
event.preventDefault()
return
}
}
if (workspace.overlay === 'outline' && listening && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (outlineKey(event)) {
event.preventDefault()
@@ -769,6 +813,11 @@
void runCommand('today')
return
}
if (key === 'C') {
event.preventDefault()
void runCommand('cal')
return
}
if (workspace.view === 'board') {
if (previous === 'g' && key === 'g') {
@@ -847,6 +896,12 @@
if (!reading || !hooks.scrollWide(amount)) workspace.say('nothing wider than the column in view', 'error')
return
}
// `[d` / `]d`: the diary entry of the day before or after, by its date.
if ((previous === '[' || previous === ']') && key === 'd') {
event.preventDefault()
void workspace.guard(() => workspace.stepEntry(previous === '[' ? -1 : 1))
return
}
if ((previous === ']' && key === ']') || (previous === '[' && key === '[')) {
event.preventDefault()
// Past the last heading there is still the end, as in vim.
@@ -1112,7 +1167,7 @@
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
<div class="overlay" tabindex="-1" bind:this={overlayEl} onclick={(e) => e.target === e.currentTarget && (workspace.overlay = 'none')}
>
{#if workspace.overlay === 'help'}<Help />{:else if workspace.overlay === 'overview'}<Overview />{:else if workspace.overlay === 'agenda'}<Agenda />{:else if workspace.overlay === 'actions'}<Actions />{:else if workspace.overlay === 'verify'}<Verify />{:else if workspace.overlay === 'outline'}<Headings />{:else if workspace.overlay === 'trash'}<Trash />{:else if workspace.overlay === 'history'}<History />{:else if workspace.overlay === 'backlinks'}<Backlinks />{:else}<MediaBrowser />{/if}
{#if workspace.overlay === 'help'}<Help />{:else if workspace.overlay === 'overview'}<Overview />{:else if workspace.overlay === 'agenda'}<Agenda />{:else if workspace.overlay === 'actions'}<Actions />{:else if workspace.overlay === 'verify'}<Verify />{:else if workspace.overlay === 'outline'}<Headings />{:else if workspace.overlay === 'trash'}<Trash />{:else if workspace.overlay === 'history'}<History />{:else if workspace.overlay === 'backlinks'}<Backlinks />{:else if workspace.overlay === 'calendar'}<Calendar />{:else}<MediaBrowser />{/if}
</div>
{/if}
</div>
+31 -2
View File
@@ -5,7 +5,7 @@
* had today yet. The keys live in App.svelte with the others; this draws
* and lets a pointer do the same.
*/
import type { AgendaCard } from './api'
import type { AgendaCard, DiaryEntry } from './api'
import { workspace } from './store.svelte'
import { runCommand } from './commands'
import Icon from './Icon.svelte'
@@ -23,6 +23,12 @@
sheet?.querySelector<HTMLElement>(`[data-index="${index}"]`)?.scrollIntoView({ block: 'nearest' })
})
/** An entry from an earlier year, in place in the diary. */
function open(entry: DiaryEntry) {
workspace.overlay = 'none'
void workspace.guard(() => workspace.openPath(entry.path))
}
/** Where a card sits in the walked order, across the groups. */
function indexOf(card: AgendaCard) {
return workspace.agendaCards.findIndex((c) => c.id === card.id)
@@ -80,10 +86,33 @@
<p class="clear">nothing late, nothing due this week, nothing in hand. <span class="kbd">c</span> captures the next thing.</p>
{/each}
{#if workspace.agenda.on_this_day.length}
<section class="past">
<h2>on this day <span class="n">{workspace.agenda.on_this_day.length}</span></h2>
<ul>
{#each workspace.agenda.on_this_day as entry, at (entry.id)}
{@const index = total + at}
<li>
<button
class="row"
class:at={index === workspace.agendaCursor}
data-index={index}
onclick={() => open(entry)}
onpointerenter={() => (workspace.agendaCursor = index)}
>
<span class="text">{entry.name.trim() || entry.excerpt || 'untitled'}</span>
<span class="due">{entry.day.slice(0, 4)}</span>
</button>
</li>
{/each}
</ul>
</section>
{/if}
<footer class="label">
<span class="kbd">j</span><span class="kbd">k</span> move
<span class="gap"></span>
<span class="kbd">↵</span> to its board
<span class="kbd">↵</span> open
<span class="gap"></span>
<span class="kbd">␣</span> done
<span class="gap"></span>
+171
View File
@@ -0,0 +1,171 @@
<script lang="ts">
/*
* `C`: the diary by month, the way a paper one is leafed through — the days
* written in marked, the day under the cursor shown below. The keys live in
* App.svelte with the others; this draws and lets a pointer do the same.
*/
import { workspace } from './store.svelte'
import { formatDay } from './util'
const WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
const month = $derived(workspace.calendarDay.slice(0, 7))
const title = $derived.by(() => {
const [y, m] = month.split('-').map(Number)
return new Date(y, m - 1, 1).toLocaleDateString(undefined, { month: 'long', year: 'numeric' }).toLowerCase()
})
/** The month's days in weeks from Monday, with blanks before the first. */
const cells = $derived.by(() => {
const [y, m] = month.split('-').map(Number)
const lead = (new Date(y, m - 1, 1).getDay() + 6) % 7
const length = new Date(y, m, 0).getDate()
const days: (string | null)[] = Array(lead).fill(null)
for (let d = 1; d <= length; d++) days.push(`${month}-${String(d).padStart(2, '0')}`)
return days
})
const today = $derived(formatDay(Math.floor(Date.now() / 1000)))
const counts = $derived(
workspace.calendarEntries.reduce((all, entry) => all.set(entry.day, (all.get(entry.day) ?? 0) + 1), new Map<string, number>()),
)
const chosen = $derived(workspace.calendarEntries.filter((entry) => entry.day === workspace.calendarDay))
function pick(day: string) {
workspace.calendarDay = day
void workspace.guard(() => workspace.openCalendarDay())
}
</script>
<div class="sheet">
<header>
<span class="label rule"><span class="marker"></span><span class="title">{title}</span> {workspace.calendarEntries.length} entries</span>
<button onclick={() => workspace.guard(() => workspace.moveCalendar(0, -1))} aria-label="previous month">‹</button>
<button onclick={() => workspace.guard(() => workspace.moveCalendar(0, 1))} aria-label="next month">›</button>
<button onclick={() => (workspace.overlay = 'none')}>esc</button>
</header>
<ol class="grid">
{#each WEEKDAYS as weekday (weekday)}
<li class="weekday label">{weekday}</li>
{/each}
{#each cells as day, index (day ?? `blank-${index}`)}
{#if day}
<li>
<button
class="day"
class:at={day === workspace.calendarDay}
class:today={day === today}
class:written={counts.has(day)}
onclick={() => pick(day)}
>
<span class="n">{Number(day.slice(8))}</span>
{#if (counts.get(day) ?? 0) > 1}<span class="many">{counts.get(day)}</span>{:else if counts.has(day)}<span class="dot"></span>{/if}
</button>
</li>
{:else}
<li></li>
{/if}
{/each}
</ol>
<section class="chosen">
<h2>{workspace.calendarDay}</h2>
{#each chosen as entry (entry.id)}
<p class="entry"><span class="name">{entry.name.trim() || entry.excerpt || 'untitled'}</span></p>
{:else}
<p class="none">nothing written — <span class="kbd">↵</span> starts it</p>
{/each}
</section>
<footer class="label">
<span class="kbd">h</span><span class="kbd">l</span> day
<span class="gap"></span>
<span class="kbd">j</span><span class="kbd">k</span> week
<span class="gap"></span>
<span class="kbd">H</span><span class="kbd">L</span> month
<span class="gap"></span>
<span class="kbd">.</span> today
<span class="gap"></span>
<span class="kbd">↵</span> open
<span class="gap"></span>
<span class="kbd">C</span><span class="kbd">esc</span> close
</footer>
</div>
<style>
.sheet {
display: flex;
flex-direction: column;
gap: 16px;
width: min(100%, 60ch);
max-height: 100%;
overflow-y: auto;
background: color-mix(in oklab, var(--hull) 97%, transparent);
border: 1px solid var(--surface-alt);
border-radius: var(--frame-radius);
padding: 20px 24px;
}
header { display: flex; align-items: center; gap: 12px; }
header .label { flex: 1; }
header .marker { background: var(--light); }
header .title { color: var(--light); }
.grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
list-style: none;
margin: 0;
padding: 0;
}
.weekday { text-align: center; font-size: 10px; color: var(--muted); }
.day {
position: relative;
display: grid;
place-items: center;
width: 100%;
aspect-ratio: 1.4;
padding: 0;
border: 1px solid transparent;
border-radius: var(--radius-s);
font-size: 13px;
letter-spacing: 0;
color: var(--subtle);
font-variant-numeric: tabular-nums;
}
.day:active { transform: none; }
.day.written { color: var(--fg); }
.day.today { border-color: var(--rim); }
.day.at { background: var(--light); color: var(--ink); }
.dot, .many { position: absolute; bottom: 4px; }
.dot { width: 5px; height: 5px; border-radius: 999px; background: var(--light); }
.many { font-size: 9px; color: var(--light); }
.day.at :is(.dot) { background: var(--ink); }
.day.at .many { color: var(--ink); }
.chosen h2 {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 6px;
font-size: 10px;
font-weight: 400;
letter-spacing: 0.22em;
color: var(--muted);
}
.chosen h2::after { content: ''; height: 1px; flex: 1; background: var(--overlay); }
.entry, .none { margin: 2px 0; font-size: 13px; }
.entry .name { font-family: var(--font-sans); font-size: 14px; color: var(--fg); }
.none { color: var(--subtle); }
footer { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 10px; }
footer .kbd { margin-right: 2px; }
footer .gap { width: 14px; }
@media (max-width: 767px), (pointer: coarse) {
footer { display: none; }
}
</style>
+5 -3
View File
@@ -32,7 +32,9 @@
['M', 'pick up the whole folder — again puts it all back'],
['Ctrl / Shift-click', 'pick a row up · every row up to it — drag them onto a folder'],
['t', 'agenda — late, due this week, in hand, on every board'],
['T', "today's diary entry, started if there is none"],
['T', "today's diary entry, started if there is none — from templates/day when the diary has one"],
['C', 'the diary by month: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day'],
['[d / ]d', 'the diary entry before / after the one open, by date'],
['Ctrl-P', 'find anything, in every space — Enter goes to the result lit'],
['b', "open this space's board"],
['c', "capture a card into this space's inbox, without leaving here"],
@@ -73,8 +75,8 @@
]
const agendaKeys: [string, string][] = [
['j / k', 'down / up the cards'],
['Enter', 'to the card, on its own board'],
['j / k', 'down / up the cards, then what was written on this day in earlier years'],
['Enter', 'to the card, on its own board — or to the entry'],
['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'],
+33
View File
@@ -183,6 +183,27 @@ export interface Agenda {
cards: AgendaCard[]
/** Whether the diary has an entry today; null when there is no diary. */
diary_today: boolean | null
/** Diary entries from this day in earlier years, the latest year first. */
on_this_day: DiaryEntry[]
}
/** A diary entry, by the local day it is about. */
export interface DiaryEntry {
id: number
name: string
/** `2026-10-20`, in the server's timezone. */
day: string
created_at: number
/** `diary/untitled`, to open it by. */
path: string
excerpt: string
}
/** A document in a space's `templates` folder. */
export interface Template {
id: number
name: string
slug: string
}
export interface BoardList {
@@ -433,8 +454,20 @@ export const api = {
name?: string
body?: string
created_at?: number
/** Start from this document, its `{{date}}` and the rest filled in. */
template_id?: number
}) => request<WorkspaceNode>('/nodes', { method: 'POST', body: body(input) }),
/** The templates of the space `id` is in. */
templates: (id: number) => request<Template[]>(`/nodes/${id}/templates`),
/** A month's diary entries: `2026-10`. */
diaryDays: (month: string) => request<DiaryEntry[]>(`/diary/days?month=${encodeURIComponent(month)}`),
/** The entries before and after a diary entry, by date. */
diaryAround: (id: number) =>
request<{ prev: DiaryEntry | null; next: DiaryEntry | null }>(`/diary/around/${id}`),
/**
* `base_updated_at`: refused with 409 if the node was saved since then.
* `restore`: the text is a revision brought back, so what it replaces is
+45 -9
View File
@@ -156,6 +156,23 @@ const spaceNames = () => workspace.spaces.map((space) => space.slug)
/** The capture tokens' names, for Tab after `:token rm`; null until fetched. */
let tokenNames: string[] | null = null
/** The templates of the space last asked about, for `:new` to complete. */
let templateCache: { space: number; names: string[] } | null = null
/** Tab cannot wait for the server: the names come on the press after the first. */
function templateNames(): string[] {
const here = workspace.spaceId
if (here === null) return []
if (templateCache?.space !== here) {
templateCache = { space: here, names: [] }
void api.templates(here).then(
(templates) => (templateCache = { space: here, names: templates.map((t) => t.slug) }),
() => (templateCache = null),
)
}
return templateCache.names
}
/** `:encrypt` and `:decrypt`: a file, or everything under a folder. */
const sealTarget = (encrypt: boolean) =>
workspace.guard(async () => {
@@ -526,23 +543,42 @@ export const commands: CommandSpec[] = [
{
name: 'new',
aliases: ['n', 'o'],
args: '[yyyy-mm-dd]',
args: '[template] [yyyy-mm-dd] [name]',
group: 'documents',
help: 'start a new document here, optionally dated',
help: "start a new document here — from one of the space's templates, dated, named; in the diary from templates/day",
complete: (arg) => (arg.includes(' ') ? [] : templateNames()),
run: async (arg) => {
await workspace.guard(async () => {
await workspace.createDocument()
if (arg) {
const at = parseDay(arg)
if (at === null) return workspace.say(`not a date: ${arg}`, 'error')
workspace.draft.created_at = at
workspace.dirty = true
await workspace.save()
const words = arg.trim().split(/\s+/).filter(Boolean)
const here = workspace.spaceId
const templates = words.length && here !== null ? await api.templates(here) : []
if (words.length && here !== null) templateCache = { space: here, names: templates.map((t) => t.slug) }
const wanted = words[0]?.toLowerCase()
const template = templates.find((t) => t.slug === wanted || t.name.toLowerCase() === wanted)
if (template) words.shift()
let created_at: number | undefined
if (words.length && /^\d{4}-\d{2}-\d{2}$/.test(words[0])) {
const at = parseDay(words[0])
if (at === null) return workspace.say(`not a date: ${words[0]}`, 'error')
created_at = at
words.shift()
}
await workspace.createDocument(words.join(' '), { created_at, template_id: template?.id })
})
hooks.focusEditor()
},
},
{
name: 'cal',
aliases: ['calendar'],
group: 'documents',
help: 'the diary by month — h/l a day, j/k a week, H/L a month, Enter opens the day or starts it',
run: () =>
workspace.guard(async () => {
if (workspace.overlay === 'calendar') return void (workspace.overlay = 'none')
await workspace.openCalendar()
}),
},
{
name: 'agenda',
aliases: ['ag'],
+62 -7
View File
@@ -9,6 +9,7 @@ import {
type Board,
type Card,
type Crumb,
type DiaryEntry,
type WorkspaceNode,
type MediaFile,
type NodeSummary,
@@ -18,7 +19,7 @@ import {
type TrashItem,
type VaultStatus,
} from './api'
import { formatBytes, formatStamp, isTouchDevice, sealedNote, type Heading } from './util'
import { formatBytes, formatDay, formatStamp, isTouchDevice, parseDay, sealedNote, type Heading } from './util'
export type Pane = 'list' | 'document'
/** The tree of documents, or the board of the space being browsed. */
@@ -282,7 +283,7 @@ class Workspace {
pane = $state<Pane>('list')
flash = $state<Flash>(null)
overlay = $state<'none' | 'help' | 'media' | 'overview' | 'agenda' | 'actions' | 'verify' | 'outline' | 'trash' | 'history' | 'backlinks'>('none')
overlay = $state<'none' | 'help' | 'media' | 'overview' | 'agenda' | 'actions' | 'verify' | 'outline' | 'trash' | 'history' | 'backlinks' | 'calendar'>('none')
/** `gO`: the headings of what is on screen, to jump between from the keyboard. */
outline = $state.raw<Heading[]>([])
outlineCursor = $state(0)
@@ -291,9 +292,13 @@ class Workspace {
* What is due or in hand on every board, kept loaded all the time: the bar
* counts from it, so what is late is in sight without going to look.
*/
agenda = $state<Agenda>({ cards: [], diary_today: null })
agenda = $state<Agenda>({ cards: [], diary_today: null, on_this_day: [] })
agendaCursor = $state(0)
/** `C`: the day the calendar's cursor is on, `2026-10-20`, and its month's entries. */
calendarDay = $state('')
calendarEntries = $state.raw<DiaryEntry[]>([])
/** A search across every space rather than the one being browsed — Ctrl-P. */
globalSearch = $state(false)
@@ -939,11 +944,15 @@ class Workspace {
else await this.openNode(selected.id, edit)
}
/** A new document is created immediately so uploads have something to attach to. */
async createDocument(name = '') {
/**
* A new document is created immediately so uploads have something to attach
* to. In the diary it starts from `templates/day` when there is one; the
* server sees to that, so capture does the same.
*/
async createDocument(name = '', from: { created_at?: number; template_id?: number } = {}) {
const parent = this.parentId
if (parent === null) return
const node = await api.createNode({ parent_id: parent, name, body: '' })
const node = await api.createNode({ parent_id: parent, name, body: '', ...from })
this.query = ''
this.globalSearch = false
await this.refresh()
@@ -1285,7 +1294,7 @@ class Workspace {
} catch {
/* the counter is a convenience; a failed refresh leaves the last one up */
}
this.agendaCursor = Math.min(this.agendaCursor, Math.max(this.agendaCards.length - 1, 0))
this.agendaCursor = Math.min(this.agendaCursor, Math.max(this.agendaCards.length + this.agenda.on_this_day.length - 1, 0))
}
/**
@@ -1302,6 +1311,52 @@ class Workspace {
this.overlay = 'agenda'
}
/** `C`: the calendar, on the day of the entry being read or else today. */
async openCalendar() {
const open = this.open
const reading = open && this.spaces.find((space) => space.id === open.path[0]?.id)?.role === 'diary'
this.calendarDay = formatDay(reading ? open.created_at : Math.floor(Date.now() / 1000))
await this.loadCalendar()
this.overlay = 'calendar'
}
async loadCalendar() {
this.calendarEntries = await api.diaryDays(this.calendarDay.slice(0, 7))
}
/** The calendar's cursor, `days` along or `months` over; a month keeps the day where it can. */
async moveCalendar(days: number, months = 0) {
const [y, m, d] = this.calendarDay.split('-').map(Number)
const last = new Date(y, m - 1 + months + 1, 0).getDate()
const date = new Date(y, m - 1 + months, Math.min(d, last) + days, 12)
const before = this.calendarDay.slice(0, 7)
this.calendarDay = formatDay(Math.floor(date.getTime() / 1000))
if (this.calendarDay.slice(0, 7) !== before) await this.loadCalendar()
}
/** Enter in the calendar: that day's entry, or a new one dated that day. */
async openCalendarDay() {
const day = this.calendarDay
const entry = this.calendarEntries.find((e) => e.day === day)
this.overlay = 'none'
if (entry) return this.openPath(entry.path)
const home = this.spaces.find((space) => space.role === 'diary')
if (!home) return this.say('there is no diary to write in', 'error')
if (this.parentId !== home.id) await this.enterSpace(home.id)
await this.createDocument('', { created_at: parseDay(day) ?? undefined })
}
/** `[d` / `]d`: the diary entry before or after the one being read. */
async stepEntry(by: -1 | 1) {
const open = this.open
if (!open) return this.say('nothing open', 'error')
const { prev, next } = await api.diaryAround(open.id)
const entry = by < 0 ? prev : next
if (!entry) return this.say(by < 0 ? 'no entry before this one' : 'no entry after this one')
await this.openPath(entry.path)
this.say(entry.day)
}
/** Enter in the agenda: to the card, on its own board. */
async goToCard(card: AgendaCard) {
this.overlay = 'none'