special: keys go to a tile, and Esc only ever leaves

Inside the special workspace the agenda and the calendar could only be
reached with Alt-h/j/k/l, which a desktop that binds Alt to its own windows
never lets through, and the same keys meant two things: t on the agenda put
the whole workspace away, t elsewhere went to it. Three esc buttons, one per
tile, all closed everything.

Now b, t and C only ever go to their tile, from anywhere. Tab and Shift-Tab
walk the tiles, as they walk the spaces elsewhere; l past the board's last
list goes on into the agenda, h in the agenda back to the board, j past its
end into the calendar. S toggles the workspace, and q and Esc leave it from
whichever tile — after leaving fullscreen first. The tile buttons are gone;
each tile's head shows its key instead, lit on the one with focus, and the
bar has a special pill beside the spaces.

A document opened from the workspace — a day of the calendar, an entry of
the agenda, today's entry, a card's notes — comes back to the tile it was
opened from on q.

The board tile shows the board of the space being browsed, or else the last
one shown, so coming from the diary, which has none, no longer leaves it
empty. Card and list actions follow the board on screen, not the browsed
space.
This commit is contained in:
nvrl committed 2026-10-21 15:53:24 +02:00
1 parent 0245402ead
commit 176aad8c6f
15 files changed
+182 -105

No files matched your search

+2 -2
View File
@@ -137,7 +137,7 @@ it; in the document pane it is that document. In the browse pane:
| `R` | rename what is highlighted — a document, a folder or a file — starting from its name (`:name!` renames the space) |
| `m` / `p` | pick nodes up — `m` again puts one back, `M` takes the whole folder — then drop them all into the folder you walk to |
| `Ctrl`/`Shift`-click | pick a row up / every row from the cursor to it; drag picked-up rows onto a folder, a crumb or a space |
| `S` | the special workspace: the board, the agenda and the calendar side by side; `Alt-h`/`j`/`k`/`l` move between them, `f` gives one the screen |
| `S` | the special workspace: the board, the agenda and the calendar side by side; `Tab` or `Alt-h`/`j`/`k`/`l` move between them, `f` gives one the screen, `Esc` leaves |
| `b` | the board of this space, in the special workspace; a digit there shows another space's |
| `t` | the agenda: what is late, due this week, or in hand, on every board, in the special workspace |
| `T` | today's diary entry, started if there is none |
@@ -244,7 +244,7 @@ start as `sftp_host_key` in the data directory.
`b`, the board door on the rail, or `:board` swaps the tree for the special
workspace — the board of the space being browsed, with the agenda and the
calendar beside it — and swaps it back. Every space has one except the
calendar beside it — and `Esc` swaps it back. Every space has one except the
diary — a journal has no backlog — and `:set board` / `:set noboard` changes
that for the space you are in (the dimmed board door on the rail does the
same). A new board starts with `backlog`, `doing` and `done`, and the columns mean
+23 -14
View File
@@ -4,30 +4,38 @@
The board, the agenda and the [calendar](Diary.md#the-calendar) share one
workspace of their own, as the desktop's special workspace holds what is
looked at in passing. `S` (or `:special`) puts it up and away again. It is
three tiles: the board on the left, the agenda over the calendar on the right.
looked at in passing. `S` (or `:special`, or the `special` pill in the bar)
puts it up and away again. It is three tiles: the board on the left, the
agenda over the calendar on the right.
![The special workspace](../images/board.png)
| key | what it does |
| --- | --- |
| `S` | put the special workspace up, or away |
| `b`, `t`, `C` | straight to the board, the agenda or the calendar; again on the one with focus puts the workspace away |
| `Alt-h` / `Alt-j` / `Alt-k` / `Alt-l` | focus the tile that way |
| `b`, `t`, `C` | to the board, the agenda or the calendar, from anywhere |
| `Tab` / `Shift-Tab` | the next / previous tile |
| `Alt-h` / `Alt-j` / `Alt-k` / `Alt-l` | the tile that way |
| `l` past the last list, `h` in the agenda, `j` past its end | on into the tile beside |
| `f` | the focused tile fills the screen; again or `Esc` to leave |
| `1` … `9` | switch space, and the board with it |
| `q`, `Esc` | back to the tree |
| `q`, `Esc` | leave, back to where you were |
The keys go to the tile with focus, which wears the light border; the other
tiles keep their cursors, dimmed. Everything in it is read again each time it
comes up.
The keys go to the tile with focus, which wears the light border and its key
lit in its head; the other tiles keep their cursors, dimmed. `b`, `t` and `C`
only ever go to their tile, and `Esc` only ever leaves — or, with a tile
filling the screen, leaves that first. A document opened from the workspace —
a card's notes, a day of the calendar, an entry of the agenda, `T` — comes
back to the tile it was opened from on `q`. Everything in it is read again
each time it comes up.
## The board
Every space except the diary has a board. `b`, the board button on the rail, or
`:board` shows it in the special workspace, and `q` goes back to the tree. The
head of the board names every space that has one, with the digit that goes
there.
`:board` shows it in the special workspace, and `Esc` goes back to the tree.
It is the board of the space being browsed, or, in a space without one such as
the diary, the last board shown. The head of the board names every space that
has one, with the digit that goes there.
A new board has three lists: `backlog`, `doing` and `done`. The lists mean
something by where they stand, not by their names:
@@ -55,7 +63,7 @@ a week fold away until `z` shows them.
| `z` | show done cards older than a week |
| `F` | a letter over every card; type it to jump there |
| `x`, `dd` | delete the card |
| `q`, `Esc` | put the special workspace away |
| `q`, `Esc` | leave the special workspace |
### Capturing a card
@@ -121,14 +129,15 @@ special workspace beside the board.
| key | what it does |
| --- | --- |
| `j` / `k` | down / up, on into the diary entries |
| `j` / `k` | down / up, on into the diary entries, then into the calendar |
| `h` | to the board |
| `Enter` | to the card on its own board, in the tile beside, or open the entry |
| `Space` | finish the card where it stands |
| `D` | due date |
| `H` / `L` | to the list left / right on its board |
| `:` | the command line with the agenda kept up, so `:repeat` acts on the card |
| `T` | today's diary entry |
| `t`, `q`, `Esc` | put the special workspace away |
| `q`, `Esc` | leave the special workspace |
The agenda opens by itself once a day when something is late or due. "Today"
is the day in `WORKSPACE_TIMEZONE`, the same as for the mails, so a browser in
+10 -9
View File
@@ -42,14 +42,14 @@ arguments and `↑` / `↓` recall earlier lines.
| `m / p` | pick nodes up, then drop them all into the folder you walk to |
| `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 |
| `S` | the special workspace: the board, the agenda and the calendar side by side — again puts it away |
| `Alt-h/j/k/l · f` | in the special workspace: to the board, the agenda or the calendar — f gives one the screen |
| `S` | the special workspace: the board, the agenda and the calendar side by side — S again, q or Esc leave it |
| `Tab · Alt-h/j/k/l` | in the special workspace: the next tile, or the one that way — l past the last list and j past the agenda go on too |
| `t` | agenda — late, due this week, in hand, on every board — in the special workspace |
| `T` | today's diary entry, started if there is none — from templates/day when the diary has one |
| `C` | the diary by month, in the special workspace: 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` | this space's board, in the special workspace — 1 … 9 there shows another space's |
| `b` | the board, in the special workspace: this space's, or the last one shown — 1 … 9 there shows another's |
| `c` | capture a card into this space's inbox, without leaving here |
| `Esc · q` | close the document, and its tile when there are several |
| `/` | full-text search, inside the current space and its text files — Tab or Ctrl-N/P walk the results, Enter opens one |
@@ -70,7 +70,7 @@ The first list is the inbox, the last list is done.
| key | what it does |
| --- | --- |
| `h / l` | to the list left / right |
| `h / l` | to the list left / right — right of the last is the agenda |
| `j / k` | down / up the cards |
| `gg / G` | first / last card of the list |
| `J / K` | move the card down / up its list |
@@ -84,20 +84,21 @@ The first list is the inbox, the last list is done.
| `F` | a letter over every card in view — type it to put the cursor there |
| `r` | read the board again, for changes made elsewhere |
| `x · dd` | delete the card (asks y/N) |
| `q · Esc` | put the special workspace away |
| `q · Esc` | leave the special workspace |
### In the agenda
| key | what it does |
| --- | --- |
| `j / k` | down / up the cards, then what was written on this day in earlier years |
| `j / k` | down / up the cards, then what was written on this day in earlier years, then into the calendar |
| `h` | to the board |
| `Enter` | to the card, on its own board beside — or to the entry |
| `Space` | done, where it stands |
| `D` | due date: fri, tm, +3d … — :due on the line |
| `:` | the line, with the agenda kept up — :repeat weekly acts on the card |
| `H / L` | to the list left / right on its board |
| `T` | today's diary entry |
| `t · q · Esc` | put the special workspace away |
| `q · Esc` | leave the special workspace — a document opened from it comes back here on q |
## Commands
@@ -123,8 +124,8 @@ do: `:w!`, `:space! <name>`, `:import!`.
| command | what it does |
| --- | --- |
| `:board` | this space's board, in the special workspace — again puts it away |
| `:special` | the special workspace: the board, the agenda and the calendar side by side — S toggles it, Alt-h/j/k/l moves between them, 1 … 9 picks the board |
| `:board` | this space's board, in the special workspace |
| `:special` | the special workspace: the board, the agenda and the calendar side by side — S puts it up and away, Tab walks its tiles, 1 … 9 picks the board |
| `:card, :c <title>` | add a card — on the board to the list under the cursor, elsewhere to this space's inbox |
| `:card!` | add a card linked to the open document — Enter on it opens the document |
| `:due <when\|->` | due date of the card — on the board, or highlighted in the agenda: today, tm, fri, +3d, +2w, 10-31, 2026-10-31 — - clears |
+1 -1
View File
@@ -26,7 +26,7 @@ agenda.
| `H` / `L` | a month back / forward |
| `.` | today |
| `Enter` | open the day's entry, or start one dated that day |
| `C`, `Esc` | put the special workspace away |
| `q`, `Esc` | leave the special workspace; an entry opened here comes back on `q` |
A dot marks a day with an entry. A number says how many when there are more
than one.
+2 -2
View File
@@ -71,9 +71,9 @@ The rail of buttons on the right does the same as the keys, for a mouse.
add a name and press `Enter`. `l` or `Enter` on the folder goes into it, and
`h` comes back up.
6. Press `c`, type `try the board`, and press `Enter`. That is a card in the
inbox of this space's board. Press `b` to see the board, and `b` again to go
inbox of this space's board. Press `b` to see the board, and `Esc` to go
back. It shares a special workspace with the agenda and the calendar: `S`
puts it up, and `Alt-h`/`Alt-l` move between them.
puts it up and away, and `Tab` walks between them.
7. Press `T`. That is today's diary entry, started for you.
8. Press `?` for the list of every key and command. `h` / `l` walk its columns, `/` narrows it down, and `Esc` closes it.
+2 -2
View File
@@ -13,7 +13,7 @@ The keys are the desktop's. On a phone, every one of these has a button; see
The first time you open the workspace each day, the **agenda** opens by itself
when anything is late or due. Any other time, press `t`. It opens in the
**special workspace**, beside the board and the calendar; `S` puts that up and
away from anywhere, and `Alt-h`/`Alt-l` move between its tiles.
away from anywhere, and `Tab` walks its tiles.
![The agenda](../images/agenda.png)
@@ -35,7 +35,7 @@ Walk it with `j` and `k`. On a card:
- `H` and `L` move it to the list left or right on its board;
- `Enter` goes to the card on its own board, in the tile beside.
`Esc` or `t` puts the special workspace away. The bar keeps showing the `late` and `today`
`Esc` leaves the special workspace. The bar keeps showing the `late` and `today`
counts while either is above zero. More in [Boards and the agenda](Boards-and-Agenda.md).
## At work: spaces, documents and tiles
+28 -13
View File
@@ -295,7 +295,8 @@
/**
* 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.
* Past the cards the cursor walks on into what was written on this day, and
* past that into the calendar under it; `h` is the board to its left.
*/
function agendaKey(event: KeyboardEvent): boolean {
const cards = workspace.agendaCards
@@ -303,19 +304,20 @@
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 + workspace.agenda.on_this_day.length - 1, 0))
case 'ArrowDown': {
const last = Math.max(cards.length + workspace.agenda.on_this_day.length - 1, 0)
if (workspace.agendaCursor >= last) workspace.specialFocus = 'calendar'
else workspace.agendaCursor += 1
return true
}
case 'k':
case 'ArrowUp':
workspace.agendaCursor = Math.max(workspace.agendaCursor - 1, 0)
return true
case 'Enter':
if (card) void workspace.guard(() => workspace.goToCard(card))
else if (past) {
workspace.closeSpecial()
void workspace.guard(() => workspace.openPath(past.path))
}
// Opened from the special workspace, so `q` on it comes back here.
else if (past) void workspace.guard(() => workspace.openPath(past.path))
return true
case ' ':
if (card) void workspace.guard(() => workspace.finishCard(card))
@@ -337,10 +339,12 @@
if (card) void workspace.guard(() => workspace.sendAgendaCard(card, event.key === 'H' ? -1 : 1))
return true
case 'T':
workspace.closeSpecial()
void runCommand('today')
return true
case 't':
case 'h':
case 'ArrowLeft':
workspace.specialFocus = 'board'
return true
case 'q':
workspace.closeSpecial()
return true
@@ -369,7 +373,6 @@
case 'Enter':
void workspace.guard(() => workspace.openCalendarDay()).then(() => hooks.focusEditor())
return true
case 'C':
case 'q':
workspace.closeSpecial()
return true
@@ -638,7 +641,9 @@
return true
case 'l':
case 'ArrowRight':
workspace.moveList(1)
// Right of the last list is the agenda, the tile beside the board.
if (workspace.listCursor >= (workspace.board?.lists.length ?? 0) - 1) workspace.specialFocus = 'agenda'
else workspace.moveList(1)
return true
case 'j':
case 'ArrowDown':
@@ -854,6 +859,13 @@
const previous = pending
pending = ''
// In the special workspace Tab walks its tiles, as it walks the spaces
// everywhere else; the digits still switch space, and the board with it.
if (key === 'Tab' && workspace.view === 'special') {
event.preventDefault()
workspace.cycleSpecial(event.shiftKey ? -1 : 1)
return
}
if (spaceKey(event)) {
event.preventDefault()
return
@@ -888,7 +900,8 @@
return
}
// `S`, as the desktop's special workspace is bound: the board, the
// agenda and the calendar, put up and away again.
// agenda and the calendar, put up and away again. `b`, `t` and `C` only
// ever go to their tile.
if (key === 'S') {
event.preventDefault()
void runCommand('special')
@@ -909,7 +922,6 @@
}
if (key === 'b') {
event.preventDefault()
workspace.closeSpecial()
return
}
if (previous === 'g' && key === 'g') {
@@ -1307,6 +1319,9 @@
.special > .agenda { grid-area: agenda; }
.special > .calendar { grid-area: calendar; }
.special .fill { height: 100%; }
/* The focused tile's key is lit, as its border is. */
.special > .window.focused :global(.tile-key) { background: var(--light); border-color: var(--light); color: var(--ink); }
/* One cursor is lit at a time: the other tiles keep theirs, dimmed. */
.special > .window:not(.focused) :global(:is(.card.active, .row.at, .day.at)) {
background: var(--surface-alt);
+4 -3
View File
@@ -25,7 +25,6 @@
/** An entry from an earlier year, in place in the diary. */
function open(entry: DiaryEntry) {
workspace.closeSpecial()
void workspace.guard(() => workspace.openPath(entry.path))
}
@@ -38,7 +37,7 @@
<div class="sheet" bind:this={sheet}>
<header>
<span class="label rule"><span class="marker"></span><span class="title">agenda</span> {total} open</span>
<button onclick={() => workspace.closeSpecial()}>esc</button>
<span class="kbd tile-key" title="the key that comes to this tile">t</span>
</header>
{#if workspace.agenda.diary_today === false}
@@ -125,7 +124,9 @@
<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
<span class="kbd">tab</span> tile
<span class="gap"></span>
<span class="kbd">esc</span> leave
</footer>
</div>
+10
View File
@@ -35,6 +35,15 @@
<button class="pill add" title="new space (:space! <name>)" onclick={() => hooks.openCommandLine(':space! ')}>
+
</button>
<!-- The special workspace, beside the others as the desktop bar shows its own. -->
<button
class="pill special"
aria-current={workspace.view === 'special'}
title="the special workspace: board, agenda, calendar (S)"
onclick={() => workspace.guard(() => workspace.toggleSpecial())}
>
<span class="slug">special</span>
</button>
</nav>
<span class="divider path"></span>
@@ -114,6 +123,7 @@
.pill:hover .n { color: var(--light-dim); }
.pill[aria-current='true'] .n { color: var(--ink); opacity: 0.55; }
.pill.add { color: var(--muted); }
.pill.special { margin-left: 6px; }
.pill.add:hover { color: var(--light); }
/* A crumb is a link along the path; a hairline grows under it on hover. */
+9 -4
View File
@@ -57,7 +57,7 @@
<div class="board">
<div class="head label rule">
<span class="marker"></span>
<span class="name">{workspace.space?.slug ?? ''}</span>
<span class="name">{workspace.boardSpace?.slug ?? ''}</span>
<span>board</span>
{#if workspace.board}<span class="count">{total} {total === 1 ? 'card' : 'cards'}</span>{/if}
<!-- Every board is a digit away: the digit switches space, and the board
@@ -65,12 +65,13 @@
{#if boards.length > 1}
<span class="boards">
{#each boards as { space, digit } (space.id)}
<button class="other" class:here={space.id === workspace.spaceId} onclick={() => workspace.guard(() => workspace.switchSpace(digit === 0 ? 9 : digit - 1))}>
<button class="other" class:here={space.id === workspace.boardSpaceId} onclick={() => workspace.guard(() => workspace.switchSpace(digit === 0 ? 9 : digit - 1))}>
<span class="n">{digit}</span>{space.slug}
</button>
{/each}
</span>
{/if}
<span class="kbd tile-key" title="the key that comes to this tile">b</span>
</div>
<div class="columns" bind:this={columns}>
@@ -132,8 +133,8 @@
</section>
{:else}
<div class="empty muted">
{#if !workspace.space?.has_board}
{workspace.space?.slug ?? 'this space'} has no board — :set board turns one on{#if boards.length}, or a digit goes to one that has{/if}.
{#if !workspace.boardSpace}
no space has a board — :set board gives the one you are in one.
{:else}
no lists yet — :list &lt;name&gt; makes a column. then o adds a card to it.
{/if}
@@ -152,6 +153,8 @@
<span><span class="kbd">R</span> rename</span>
<span><span class="kbd">dd</span> delete</span>
<span><span class="kbd">1</span>…<span class="kbd">9</span> board</span>
<span><span class="kbd">tab</span> tile</span>
<span><span class="kbd">esc</span> leave</span>
<span class="rule-hint">first list is the inbox · last list is done</span>
</footer>
</div>
@@ -172,6 +175,8 @@
.head .marker { background: var(--light); }
.head .name { color: var(--light); }
.boards { display: flex; gap: 4px; margin-left: 8px; }
/* After the rule, at the right end, where the other tiles have theirs. */
.head .tile-key { order: 1; margin-left: 12px; }
.other {
display: flex;
gap: 6px;
+4 -2
View File
@@ -42,7 +42,7 @@
<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.closeSpecial()}>esc</button>
<span class="kbd tile-key" title="the key that comes to this tile">C</span>
</header>
<ol class="grid">
@@ -89,7 +89,9 @@
<span class="gap"></span>
<span class="kbd">↵</span> open
<span class="gap"></span>
<span class="kbd">C</span><span class="kbd">esc</span> close
<span class="kbd">tab</span> tile
<span class="gap"></span>
<span class="kbd">esc</span> leave
</footer>
</div>
+8 -7
View File
@@ -72,14 +72,14 @@
['m / p', 'pick nodes up, then drop them all into the folder you walk to'],
['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'],
['S', 'the special workspace: the board, the agenda and the calendar side by side — again puts it away'],
['Alt-h/j/k/l · f', 'in the special workspace: to the board, the agenda or the calendar — f gives one the screen'],
['S', 'the special workspace: the board, the agenda and the calendar side by side — S again, q or Esc leave it'],
['Tab · Alt-h/j/k/l', 'in the special workspace: the next tile, or the one that way — l past the last list and j past the agenda go on too'],
['t', 'agenda — late, due this week, in hand, on every board — in the special workspace'],
['T', "today's diary entry, started if there is none — from templates/day when the diary has one"],
['C', 'the diary by month, in the special workspace: 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', "this space's board, in the special workspace — 1 … 9 there shows another space's"],
['b', "the board, in the special workspace: this space's, or the last one shown — 1 … 9 there shows another's"],
['c', "capture a card into this space's inbox, without leaving here"],
['Esc · q', 'close the document, and its tile when there are several'],
['/', 'full-text search, inside the current space and its text files — Tab or Ctrl-N/P walk the results, Enter opens one'],
@@ -100,7 +100,7 @@
footnotes on the list keys. */
const boardKeys: [string, string][] = [
['rule', 'the first list is the inbox, the last list is done'],
['h / l', 'to the list left / right'],
['h / l', 'to the list left / right — right of the last is the agenda'],
['j / k', 'down / up the cards'],
['gg / G', 'first / last card of the list'],
['J / K', 'move the card down / up its list'],
@@ -114,18 +114,19 @@
['F', 'a letter over every card in view — type it to put the cursor there'],
['r', 'read the board again, for changes made elsewhere'],
['x · dd', 'delete the card (asks y/N)'],
['q · Esc', 'put the special workspace away'],
['q · Esc', 'leave the special workspace'],
]
const agendaKeys: [string, string][] = [
['j / k', 'down / up the cards, then what was written on this day in earlier years'],
['j / k', 'down / up the cards, then what was written on this day in earlier years, then into the calendar'],
['h', 'to the board'],
['Enter', 'to the card, on its own board beside — or to the entry'],
['Space', 'done, where it stands'],
['D', 'due date: fri, tm, +3d … — :due on the line'],
[':', 'the line, with the agenda kept up — :repeat weekly acts on the card'],
['H / L', 'to the list left / right on its board'],
['T', "today's diary entry"],
['t · q · Esc', 'put the special workspace away'],
['q · Esc', 'leave the special workspace — a document opened from it comes back here on q'],
]
const titles: Record<Group, string> = {
+1 -1
View File
@@ -37,7 +37,7 @@
{ icon: 'board', label: 'Board', keys: ['b'], run: () => (workspace.specialFocus = 'board'), on: workspace.specialFocus === 'board' },
{ icon: 'agenda', label: 'Agenda — late, due, in hand', keys: ['t'], run: () => (workspace.specialFocus = 'agenda'), on: workspace.specialFocus === 'agenda' },
{ icon: 'calendar', label: 'Calendar', keys: ['C'], run: () => (workspace.specialFocus = 'calendar'), on: workspace.specialFocus === 'calendar' },
{ icon: 'list', label: 'Back to the tree', keys: ['S', 'q'], run: () => workspace.closeSpecial() },
{ icon: 'list', label: 'Back to the tree', keys: ['Esc', 'S'], run: () => workspace.closeSpecial() },
]
if (workspace.specialFocus !== 'board') return [tiles, [cmd, help]]
// The card verbs are doors too, so a phone — which has no H, L or
+7 -7
View File
@@ -332,7 +332,7 @@ export const commands: CommandSpec[] = [
run: async () => {
// A document `o` made and nothing was written in is not kept.
if (await workspace.abandonIfBlank()) {
if (workspace.returnTo === 'board') await workspace.guard(() => workspace.returnToBoard())
if (workspace.returnTo === 'special') await workspace.guard(() => workspace.returnToSpecial())
return hooks.focusList()
}
if (workspace.dirty) {
@@ -340,7 +340,7 @@ export const commands: CommandSpec[] = [
return
}
if (workspace.editing) workspace.editing = false
else if (workspace.returnTo === 'board') return void workspace.guard(() => workspace.returnToBoard())
else if (workspace.returnTo === 'special') return void workspace.guard(() => workspace.returnToSpecial())
else if (workspace.tiles.length > 1) return focusTile(closeFocused())
else workspace.closeTile()
hooks.focusList()
@@ -429,13 +429,13 @@ export const commands: CommandSpec[] = [
{
name: 'board',
group: 'board',
help: "this space's board, in the special workspace — again puts it away",
run: () => workspace.guard(() => workspace.toggleSpecial('board')),
help: "this space's board, in the special workspace",
run: () => workspace.guard(() => workspace.showSpecial('board')),
},
{
name: 'special',
group: 'board',
help: 'the special workspace: the board, the agenda and the calendar side by side — S toggles it, Alt-h/j/k/l moves between them, 1 … 9 picks the board',
help: 'the special workspace: the board, the agenda and the calendar side by side — S puts it up and away, Tab walks its tiles, 1 … 9 picks the board',
run: () => workspace.guard(() => workspace.toggleSpecial()),
},
{
@@ -599,7 +599,7 @@ export const commands: CommandSpec[] = [
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.view === 'special') return workspace.toggleSpecial('calendar')
if (workspace.view === 'special') return void (workspace.specialFocus = 'calendar')
await workspace.openCalendar()
}),
},
@@ -611,7 +611,7 @@ export const commands: CommandSpec[] = [
run: () =>
workspace.guard(async () => {
if (workspace.view !== 'special') workspace.agendaCursor = 0
await workspace.toggleSpecial('agenda')
await workspace.showSpecial('agenda')
}),
},
{
+71 -38
View File
@@ -76,7 +76,7 @@ export class Tile {
* document goes back to the board — on the same card — rather than leaving
* you in a tree you never asked for.
*/
returnTo = $state<'board' | null>(null)
returnTo = $state<'special' | null>(null)
/*
* The document `o` just made. If it is left with neither a name nor a word
* in it, it is removed on the way out rather than left as an empty row.
@@ -170,7 +170,7 @@ class Workspace {
get returnTo() {
return this.tile.returnTo
}
set returnTo(to: 'board' | null) {
set returnTo(to: 'special' | null) {
this.tile.returnTo = to
}
get vimMode() {
@@ -299,6 +299,12 @@ class Workspace {
overlay = $state<'none' | 'help' | 'media' | 'overview' | 'actions' | 'verify' | 'outline' | 'trash' | 'history' | 'backlinks'>('none')
/** Which of the special workspace's tiles the keys go to. */
specialFocus = $state<SpecialTile>('board')
/**
* The space whose board the special workspace shows: the one being browsed
* when it has a board, or else the last one shown, so that coming from the
* diary does not leave the board's tile empty.
*/
boardSpaceId = $state<number | null>(null)
/** `gO`: the headings of what is on screen, to jump between from the keyboard. */
outline = $state.raw<Heading[]>([])
outlineCursor = $state(0)
@@ -821,14 +827,12 @@ class Workspace {
this.cursor = Math.min(spot.cursor, Math.max(this.nodes.length - 1, 0))
}
}
// The board belongs to the space, so switching space with the special
// workspace up shows the new space's board — or says it has none, rather
// than leaving the previous board on screen.
if (this.view === 'special') {
// Switching space with the special workspace up shows the new space's
// board; a space without one leaves the last board where it is.
if (this.view === 'special' && this.space?.has_board) {
this.listCursor = 0
this.cardCursor = 0
if (this.space?.has_board) await this.reloadBoard(id, undefined)
else this.rawBoard = null
await this.reloadBoard(id, undefined)
}
}
@@ -910,7 +914,7 @@ class Workspace {
if (isContainer(node)) this.closeSpecial()
else {
this.view = 'tree'
this.returnTo = 'board'
this.returnTo = 'special'
}
}
if (isContainer(node)) {
@@ -1222,6 +1226,7 @@ class Workspace {
for (const tile of this.tiles) tile.clear()
this.prune()
this.rawBoard = null
this.boardSpaceId = null
this.view = 'tree'
this.spaces = await api.listSpaces()
const next = this.spaces.find((space) => space.role === 'diary') ?? this.spaces[0]
@@ -1351,7 +1356,7 @@ class Workspace {
async openCalendarDay() {
const day = this.calendarDay
const entry = this.calendarEntries.find((e) => e.day === day)
this.closeSpecial()
// Opened from the special workspace, so `q` on the entry comes back here.
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')
@@ -1384,7 +1389,7 @@ class Workspace {
if (!done) return
const moved = await api.moveCard(card.id, done.id, done.cards.length)
await this.loadAgenda()
if (this.view === 'special' && this.spaceId === card.space_id) await this.reloadBoard()
if (this.view === 'special' && this.boardSpaceId === card.space_id) await this.reloadBoard()
this.say(doneSaid(moved))
}
@@ -1397,7 +1402,7 @@ class Workspace {
// 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 === 'special' && this.spaceId === card.space_id) await this.reloadBoard()
if (this.view === 'special' && this.boardSpaceId === card.space_id) await this.reloadBoard()
return updated
}
@@ -1414,7 +1419,7 @@ class Workspace {
await this.loadAgenda()
const at = this.agendaCards.findIndex((c) => c.id === card.id)
if (at >= 0) this.agendaCursor = at
if (this.view === 'special' && this.spaceId === card.space_id) await this.reloadBoard()
if (this.view === 'special' && this.boardSpaceId === card.space_id) await this.reloadBoard()
this.say(from + delta === lists.length - 1 ? doneSaid(moved) : `"${card.title}" → ${target.name}`)
}
@@ -1489,13 +1494,18 @@ class Workspace {
*/
async openSpecial(focus: SpecialTile = this.specialFocus) {
const space = this.space
if (space?.has_board) await this.reloadBoard(space.id)
else this.rawBoard = null
const shown = this.boardSpace?.has_board ? this.boardSpace : this.spaces.find((s) => s.has_board)
const board = space?.has_board ? space : shown
if (board) await this.reloadBoard(board.id, board.id === this.boardSpaceId ? this.card?.id : undefined)
else {
this.rawBoard = null
this.boardSpaceId = null
}
if (!this.calendarDay) this.calendarDay = formatDay(Math.floor(Date.now() / 1000))
await Promise.all([this.loadAgenda(), this.loadCalendar()])
this.view = 'special'
this.specialFocus = focus
if (focus === 'board' && space && !space.has_board) this.say(`${space.slug} has no board — :set board turns one on`, 'error')
if (focus === 'board' && !board) this.say('no space has a board — :set board turns one on', 'error')
}
closeSpecial() {
@@ -1503,24 +1513,40 @@ class Workspace {
this.returnTo = null
}
/** `S`: the special workspace up, on the tile it was left on, or away again. */
async toggleSpecial() {
if (this.view === 'special') this.closeSpecial()
else await this.openSpecial()
}
/**
* `b`, `t`, `C` and `S`: the special workspace on that tile. A key whose
* tile already has focus puts the workspace away again, as the desktop
* toggles its scratchpad; any other moves the focus there.
* `b`, `t` and `C`: to that tile of the special workspace, putting it up
* first when it is not. They only ever go somewhere; leaving is `S`,
* `q` or `Esc`, from whichever tile.
*/
async toggleSpecial(focus?: SpecialTile) {
if (this.view !== 'special') return this.openSpecial(focus)
if (focus === undefined || focus === this.specialFocus) return this.closeSpecial()
this.specialFocus = focus
async showSpecial(focus: SpecialTile) {
if (this.view === 'special') this.specialFocus = focus
else await this.openSpecial(focus)
}
/** `Tab` / `Shift-Tab` in the special workspace: the next tile round, or the one before. */
cycleSpecial(by: 1 | -1) {
const order: SpecialTile[] = ['board', 'agenda', 'calendar']
this.specialFocus = order[(order.indexOf(this.specialFocus) + by + order.length) % order.length]
}
async openBoard() {
await this.openSpecial('board')
}
async reloadBoard(spaceId = this.spaceId, keep = this.card?.id) {
get boardSpace(): NodeSummary | undefined {
return this.spaces.find((space) => space.id === this.boardSpaceId)
}
async reloadBoard(spaceId = this.boardSpaceId ?? this.spaceId, keep = this.card?.id) {
if (spaceId === null) return
this.rawBoard = await api.board(spaceId)
this.boardSpaceId = spaceId
void this.loadAgenda()
// Follow the card that was just moved rather than whatever now sits under
// the cursor, so J J J walks one card down instead of shuffling three.
@@ -1576,7 +1602,7 @@ class Workspace {
// The neighbour's own position, not its index: older done cards may be
// hidden above it.
await api.moveCard(card.id, list.id, list.cards[to].position)
await this.reloadBoard(this.spaceId, card.id)
await this.reloadBoard(this.boardSpaceId, card.id)
}
/** `H`/`L`: the same card, one list over — the move a board is really for. */
@@ -1594,7 +1620,7 @@ class Workspace {
const target = lists[index]
if (!card || !target || index === this.listCursor) return
const moved = await api.moveCard(card.id, target.id, target.cards.length)
await this.reloadBoard(this.spaceId, card.id)
await this.reloadBoard(this.boardSpaceId, card.id)
this.say(index === lists.length - 1 ? doneSaid(moved) : `"${card.title}" → ${target.name}`)
}
@@ -1609,7 +1635,7 @@ class Workspace {
const list = this.list
if (!list) return this.say('no list to put it on', 'error')
const card = await api.createCard(list.id, { title, node_id: nodeId })
await this.reloadBoard(this.spaceId, card.id)
await this.reloadBoard(this.boardSpaceId, card.id)
return this.say(nodeId === null ? `carded "${card.title}"` : `carded "${card.title}" → #${nodeId}`)
}
@@ -1626,7 +1652,7 @@ class Workspace {
const card = this.card
if (!card) return this.say('no card selected', 'error')
const updated = await api.updateCard(card.id, patch)
await this.reloadBoard(this.spaceId, card.id)
await this.reloadBoard(this.boardSpaceId, card.id)
return updated
}
@@ -1649,13 +1675,13 @@ class Workspace {
const card = this.card
if (!card) return this.say('no card selected', 'error')
await api.deleteCard(card.id)
await this.reloadBoard(this.spaceId, undefined)
await this.reloadBoard(this.boardSpaceId, undefined)
this.say(`deleted "${card.title}"`)
}
async createList(name: string) {
if (this.spaceId === null) return
this.rawBoard = await api.createList(this.spaceId, name)
if (this.boardSpaceId === null) return
this.rawBoard = await api.createList(this.boardSpaceId, name)
// It goes in front of the done list, so it is not simply the last one.
const names = this.rawBoard.lists.map((list) => list.name)
this.listCursor = Math.max(names.lastIndexOf(name), 0)
@@ -1686,7 +1712,7 @@ class Workspace {
*/
async openCard() {
const card = this.card
const space = this.spaceId
const space = this.boardSpaceId
if (!card || space === null) return
let nodeId = card.node_id
if (nodeId !== null) {
@@ -1718,16 +1744,19 @@ class Workspace {
await this.openNode(nodeId, card.node_id === null)
this.prune()
if (card.node_id === null) this.enterInsert = true
this.returnTo = 'board'
this.returnTo = 'special'
}
/** Back to the board a card's document was opened from. */
async returnToBoard() {
/**
* Back to the special workspace a document was opened from — a card's
* notes, a day of the calendar, an entry of the agenda — on the tile it
* was opened from.
*/
async returnToSpecial() {
this.returnTo = null
this.closeTile()
this.pane = 'list'
await this.reloadBoard()
this.view = 'special'
await this.openSpecial(this.specialFocus)
}
/** Turn a space's board on or off. The diary ships with it off. */
@@ -1737,7 +1766,11 @@ class Workspace {
await api.updateNode(space.id, { has_board: on })
this.spaces = await api.listSpaces()
if (on) await this.openBoard()
else this.rawBoard = null
else if (this.boardSpaceId === space.id) {
this.rawBoard = null
this.boardSpaceId = null
if (this.view === 'special') await this.openSpecial()
}
this.say(on ? `${space.slug} has a board` : `${space.slug} has no board any more`)
}