special: the board, the agenda and the calendar in a workspace of their own

The board swapped out the tree, and the agenda and the calendar were
overlays over it, each with a key to open and a key to close. They are now
three tiles of one special workspace, as the desktop keeps its scratchpad:
the board on the left, the agenda over the calendar on the right.

S puts it up and away from anywhere. b, t and C go straight to their tile,
and put the workspace away when that tile already has focus. Alt-h/j/k/l
move the focus between the tiles, f gives the focused one the porthole,
and the tiles without focus keep their cursors, dimmed. A digit switches
space as everywhere, and the board follows; the board's head names every
space that has one. Enter on an agenda card goes to it on the board beside.
A space without a board says so in the board's tile instead of refusing.

The rail offers the three tiles as doors, so a phone, which shows one at a
time, can walk between them. The help, the README, the wiki and the
screenshots say how it works now.
This commit is contained in:
nvrl committed 2026-10-21 15:04:58 +02:00
1 parent 304c67151c
commit 73ac961751
31 files changed
+321 -117

No files matched your search

+6 -4
View File
@@ -136,8 +136,9 @@ 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) | | `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 | | `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 | | `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 |
| `b` | the board of this 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 |
| `t` | the agenda: what is late, due this week, or in hand, on every board | | `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 | | `T` | today's diary entry, started if there is none |
| `C` | the diary by month (`:cal`) — see [The diary](#the-diary) | | `C` | the diary by month (`:cal`) — see [The diary](#the-diary) |
| `[d` / `]d` | the diary entry before / after the one open, by date | | `[d` / `]d` | the diary entry before / after the one open, by date |
@@ -240,8 +241,9 @@ start as `sftp_host_key` in the data directory.
## Boards ## Boards
`b`, the board door on the rail, or `:board` swaps the tree for the board `b`, the board door on the rail, or `:board` swaps the tree for the special
of the space being browsed, and swaps it back. Every space has one except the 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
diary — a journal has no backlog — and `:set board` / `:set noboard` changes 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 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 same). A new board starts with `backlog`, `doing` and `done`, and the columns mean
Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

+31 -8
View File
@@ -1,11 +1,33 @@
# Boards and the agenda # Boards and the agenda
## The special workspace
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.
![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 |
| `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 |
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 board ## The board
Every space except the diary has a board. `b`, the board button on the rail, or Every space except the diary has a board. `b`, the board button on the rail, or
`:board` swaps the tree for it, and `q` swaps back. `: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
![The board of a space](../images/board.png) there.
A new board has three lists: `backlog`, `doing` and `done`. The lists mean A new board has three lists: `backlog`, `doing` and `done`. The lists mean
something by where they stand, not by their names: something by where they stand, not by their names:
@@ -33,11 +55,11 @@ a week fold away until `z` shows them.
| `z` | show done cards older than a week | | `z` | show done cards older than a week |
| `F` | a letter over every card; type it to jump there | | `F` | a letter over every card; type it to jump there |
| `x`, `dd` | delete the card | | `x`, `dd` | delete the card |
| `q`, `Esc` | back to the tree | | `q`, `Esc` | put the special workspace away |
### Capturing a card ### Capturing a card
`c` anywhere outside the board opens `:card ` on the command line. The card goes `c` anywhere outside the special workspace opens `:card ` on the command line. The card goes
into the inbox of the space you are in, or of the first space with a board, and into the inbox of the space you are in, or of the first space with a board, and
you stay where you were. From outside the browser, see [Capture](Capture.md). you stay where you were. From outside the browser, see [Capture](Capture.md).
@@ -86,7 +108,8 @@ does not make a second copy. The board and the agenda show the rule as
## The agenda ## The agenda
`t` (or `:agenda`) gathers what needs you from every board at once. `t` (or `:agenda`) gathers what needs you from every board at once, in the
special workspace beside the board.
![The agenda](../images/agenda.png) ![The agenda](../images/agenda.png)
@@ -99,13 +122,13 @@ does not make a second copy. The board and the agenda show the rule as
| key | what it does | | key | what it does |
| --- | --- | | --- | --- |
| `j` / `k` | down / up, on into the diary entries | | `j` / `k` | down / up, on into the diary entries |
| `Enter` | to the card on its own board, or open the entry | | `Enter` | to the card on its own board, in the tile beside, or open the entry |
| `Space` | finish the card where it stands | | `Space` | finish the card where it stands |
| `D` | due date | | `D` | due date |
| `H` / `L` | to the list left / right on its board | | `H` / `L` | to the list left / right on its board |
| `:` | the command line with the agenda kept up, so `:repeat` acts on the card | | `:` | the command line with the agenda kept up, so `:repeat` acts on the card |
| `T` | today's diary entry | | `T` | today's diary entry |
| `t`, `q`, `Esc` | close | | `t`, `q`, `Esc` | put the special workspace away |
The agenda opens by itself once a day when something is late or due. "Today" 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 is the day in `WORKSPACE_TIMEZONE`, the same as for the mails, so a browser in
+10 -7
View File
@@ -42,12 +42,14 @@ arguments and `↑` / `↓` recall earlier lines.
| `m / p` | pick nodes up, then drop them all into the folder you walk to | | `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 | | `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 | | `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 | | `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 |
| `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 | | `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 | | `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 | | `[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 | | `Ctrl-P` | find anything, in every space — Enter goes to the result lit |
| `b` | open this space's board | | `b` | this space's board, in the special workspace — 1 … 9 there shows another space's |
| `c` | capture a card into this space's inbox, without leaving here | | `c` | capture a card into this space's inbox, without leaving here |
| `Esc · q` | close the document, and its tile when there are several | | `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 | | `/` | full-text search, inside the current space and its text files — Tab or Ctrl-N/P walk the results, Enter opens one |
@@ -82,20 +84,20 @@ 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 | | `F` | a letter over every card in view — type it to put the cursor there |
| `r` | read the board again, for changes made elsewhere | | `r` | read the board again, for changes made elsewhere |
| `x · dd` | delete the card (asks y/N) | | `x · dd` | delete the card (asks y/N) |
| `q · Esc` | back to the tree | | `q · Esc` | put the special workspace away |
### In the agenda ### In the agenda
| key | what it does | | 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 |
| `Enter` | to the card, on its own board — or to the entry | | `Enter` | to the card, on its own board beside — or to the entry |
| `Space` | done, where it stands | | `Space` | done, where it stands |
| `D` | due date: fri, tm, +3d … — :due on the line | | `D` | due date: fri, tm, +3d … — :due on the line |
| `:` | the line, with the agenda kept up — :repeat weekly acts on the card | | `:` | the line, with the agenda kept up — :repeat weekly acts on the card |
| `H / L` | to the list left / right on its board | | `H / L` | to the list left / right on its board |
| `T` | today's diary entry | | `T` | today's diary entry |
| `t · q · Esc` | close the agenda | | `t · q · Esc` | put the special workspace away |
## Commands ## Commands
@@ -121,7 +123,8 @@ do: `:w!`, `:space! <name>`, `:import!`.
| command | what it does | | command | what it does |
| --- | --- | | --- | --- |
| `:board` | show this space's board, or go back to the tree | | `: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 |
| `:card, :c <title>` | add a card — on the board to the list under the cursor, elsewhere to this space's inbox | | `: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 | | `: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 | | `: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 |
+4 -2
View File
@@ -13,7 +13,9 @@ an entry written after midnight on a trip still lands on the right day.
## The calendar ## The calendar
`C` (or `:cal`) shows the diary a month at a time. `C` (or `:cal`) shows the diary a month at a time, in the
[special workspace](Boards-and-Agenda.md#the-special-workspace) under the
agenda.
![The diary by month](../images/calendar.png) ![The diary by month](../images/calendar.png)
@@ -24,7 +26,7 @@ an entry written after midnight on a trip still lands on the right day.
| `H` / `L` | a month back / forward | | `H` / `L` | a month back / forward |
| `.` | today | | `.` | today |
| `Enter` | open the day's entry, or start one dated that day | | `Enter` | open the day's entry, or start one dated that day |
| `C`, `Esc` | close | | `C`, `Esc` | put the special workspace away |
A dot marks a day with an entry. A number says how many when there are more A dot marks a day with an entry. A number says how many when there are more
than one. than one.
+2 -1
View File
@@ -72,7 +72,8 @@ The rail of buttons on the right does the same as the keys, for a mouse.
`h` comes back up. `h` comes back up.
6. Press `c`, type `try the board`, and press `Enter`. That is a card in the 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 `b` again to go
back. back. It shares a special workspace with the agenda and the calendar: `S`
puts it up, and `Alt-h`/`Alt-l` move between them.
7. Press `T`. That is today's diary entry, started for you. 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. 8. Press `?` for the list of every key and command. `h` / `l` walk its columns, `/` narrows it down, and `Esc` closes it.
+7 -4
View File
@@ -11,7 +11,9 @@ The keys are the desktop's. On a phone, every one of these has a button; see
## Morning: what needs you ## Morning: what needs you
The first time you open the workspace each day, the **agenda** opens by itself 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`. 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.
![The agenda](../images/agenda.png) ![The agenda](../images/agenda.png)
@@ -31,9 +33,9 @@ Walk it with `j` and `k`. On a card:
- `Space` finishes it, where it stands; - `Space` finishes it, where it stands;
- `D` gives it a due date: `fri`, `tm`, `+3d`, `10-31`; - `D` gives it a due date: `fri`, `tm`, `+3d`, `10-31`;
- `H` and `L` move it to the list left or right on its board; - `H` and `L` move it to the list left or right on its board;
- `Enter` goes to the card on its own board. - `Enter` goes to the card on its own board, in the tile beside.
`Esc` or `t` closes the agenda. The bar keeps showing the `late` and `today` `Esc` or `t` puts the special workspace away. The bar keeps showing the `late` and `today`
counts while either is above zero. More in [Boards and the agenda](Boards-and-Agenda.md). counts while either is above zero. More in [Boards and the agenda](Boards-and-Agenda.md).
## At work: spaces, documents and tiles ## At work: spaces, documents and tiles
@@ -84,7 +86,8 @@ In a meeting someone asks you to renew a certificate. Press `c`, type
`Renew the gateway certificate`, and `Enter`. The card is in this space's `Renew the gateway certificate`, and `Enter`. The card is in this space's
inbox, and you have not left the document you were reading. inbox, and you have not left the document you were reading.
Press `b` to see the **board**. Press `b` to see the **board**, in the special workspace. A digit there
shows the board of another space.
![The board](../images/board.png) ![The board](../images/board.png)
+118 -20
View File
@@ -25,7 +25,7 @@
import Shared from './lib/Shared.svelte' import Shared from './lib/Shared.svelte'
import Sidebar from './lib/Sidebar.svelte' import Sidebar from './lib/Sidebar.svelte'
import StatusBar from './lib/StatusBar.svelte' import StatusBar from './lib/StatusBar.svelte'
import { workspace } from './lib/store.svelte' import { workspace, type SpecialTile } from './lib/store.svelte'
import { dwindle, formatDay, neighbour, type Direction } from './lib/util' import { dwindle, formatDay, neighbour, type Direction } from './lib/util'
/* /*
@@ -220,8 +220,8 @@
[['j', 'k'], 'browse, l opens, L opens beside'], [['j', 'k'], 'browse, l opens, L opens beside'],
[['1…9'], 'switch space, Tab cycles'], [['1…9'], 'switch space, Tab cycles'],
[['w'], 'overview of every space'], [['w'], 'overview of every space'],
[['b'], "this space's board"], [['S'], 'board, agenda and calendar'],
[['t'], 'agenda — late, due, in hand'], [['b', 't', 'C'], 'straight to one of them'],
[['c'], 'capture a card to the inbox'], [['c'], 'capture a card to the inbox'],
[['⌃ P'], 'find in every space'], [['⌃ P'], 'find in every space'],
[['/'], 'search this space'], [['/'], 'search this space'],
@@ -313,7 +313,7 @@
case 'Enter': case 'Enter':
if (card) void workspace.guard(() => workspace.goToCard(card)) if (card) void workspace.guard(() => workspace.goToCard(card))
else if (past) { else if (past) {
workspace.overlay = 'none' workspace.closeSpecial()
void workspace.guard(() => workspace.openPath(past.path)) void workspace.guard(() => workspace.openPath(past.path))
} }
return true return true
@@ -337,12 +337,12 @@
if (card) void workspace.guard(() => workspace.sendAgendaCard(card, event.key === 'H' ? -1 : 1)) if (card) void workspace.guard(() => workspace.sendAgendaCard(card, event.key === 'H' ? -1 : 1))
return true return true
case 'T': case 'T':
workspace.overlay = 'none' workspace.closeSpecial()
void runCommand('today') void runCommand('today')
return true return true
case 't': case 't':
case 'q': case 'q':
workspace.overlay = 'none' workspace.closeSpecial()
return true return true
default: default:
return false return false
@@ -371,7 +371,7 @@
return true return true
case 'C': case 'C':
case 'q': case 'q':
workspace.overlay = 'none' workspace.closeSpecial()
return true return true
default: default:
return false return false
@@ -584,7 +584,22 @@
* too; left of the leftmost tile is the list. With Shift the focused tile * too; left of the leftmost tile is the list. With Shift the focused tile
* goes that way instead, swapping places with the one it lands on. * goes that way instead, swapping places with the one it lands on.
*/ */
/*
* The special workspace is three tiles: the board on the left, the agenda
* over the calendar on the right. Alt-h/j/k/l moves between them.
*/
const specialNeighbours: Record<SpecialTile, Partial<Record<Direction, SpecialTile>>> = {
board: { l: 'agenda' },
agenda: { h: 'board', j: 'calendar' },
calendar: { h: 'board', k: 'agenda' },
}
function tileKey(direction: Direction, shift: boolean) { function tileKey(direction: Direction, shift: boolean) {
if (workspace.view === 'special') {
const next = specialNeighbours[workspace.specialFocus][direction]
if (next && !shift) workspace.specialFocus = next
return
}
if (workspace.pane === 'list' || !workspace.open) { if (workspace.pane === 'list' || !workspace.open) {
if (direction === 'l' && !shift) focusPane('document') if (direction === 'l' && !shift) focusPane('document')
return return
@@ -681,7 +696,7 @@
return true return true
case 'q': case 'q':
case 'Escape': case 'Escape':
workspace.closeBoard() workspace.closeSpecial()
return true return true
default: default:
return false return false
@@ -734,7 +749,10 @@
return return
} }
} }
if (workspace.overlay === 'agenda' && listening && !event.ctrlKey && !event.metaKey && !event.altKey) { // The special workspace's agenda and calendar read their own keys while
// they have focus; what they leave falls through to the rest below.
const special = workspace.overlay === 'none' && workspace.view === 'special' && listening && !event.ctrlKey && !event.metaKey && !event.altKey
if (special && workspace.specialFocus === 'agenda') {
if (agendaKey(event)) { if (agendaKey(event)) {
event.preventDefault() event.preventDefault()
return return
@@ -764,7 +782,7 @@
return return
} }
} }
if (workspace.overlay === 'calendar' && listening && !event.ctrlKey && !event.metaKey && !event.altKey) { if (special && workspace.specialFocus === 'calendar') {
if (calendarKey(event)) { if (calendarKey(event)) {
event.preventDefault() event.preventDefault()
return return
@@ -842,7 +860,7 @@
} }
// `gt` / `gT` walk the tiles, as they walk vim's tab pages — before `t` // `gt` / `gT` walk the tiles, as they walk vim's tab pages — before `t`
// and `T` on their own are taken for the agenda and today's entry. // and `T` on their own are taken for the agenda and today's entry.
if (previous === 'g' && (key === 't' || key === 'T') && workspace.view !== 'board') { if (previous === 'g' && (key === 't' || key === 'T') && workspace.view !== 'special') {
event.preventDefault() event.preventDefault()
void cycleTile(key === 't' ? 1 : -1) void cycleTile(key === 't' ? 1 : -1)
return return
@@ -869,8 +887,31 @@
void runCommand('cal') void runCommand('cal')
return return
} }
// `S`, as the desktop's special workspace is bound: the board, the
// agenda and the calendar, put up and away again.
if (key === 'S') {
event.preventDefault()
void runCommand('special')
return
}
if (workspace.view === 'board') { if (workspace.view === 'special') {
if (workspace.specialFocus !== 'board') {
// The agenda and the calendar have had their keys; nothing reaches
// the board's from them, since it is not the tile with focus.
if (key === 'b') void runCommand('board')
else if (key === 'Escape') workspace.closeSpecial()
else if (key === ':') cmdline = ':'
else if (key === '?') void runCommand('help')
else return
event.preventDefault()
return
}
if (key === 'b') {
event.preventDefault()
workspace.closeSpecial()
return
}
if (previous === 'g' && key === 'g') { if (previous === 'g' && key === 'g') {
event.preventDefault() event.preventDefault()
workspace.cardCursor = 0 workspace.cardCursor = 0
@@ -1142,10 +1183,26 @@
<!-- Keyed on the space, so a switch slides the new one in, as the desktop <!-- Keyed on the space, so a switch slides the new one in, as the desktop
does between workspaces: from below when moving right, above when left. --> does between workspaces: from below when moving right, above when left. -->
{#key workspace.spaceId} {#key workspace.spaceId}
{#if workspace.view === 'board'} {#if workspace.view === 'special'}
<!-- The board takes the whole width: its columns are the two panes. --> <!-- The special workspace: the board, and the agenda over the
<div class="panes board switch" style="--dir: {workspace.switchDirection}"> calendar beside it. `f` gives the focused one the whole porthole. -->
<div class="window focused" bind:this={boardEl}><Board /></div> <div class="panes special switch" class:full={workspace.fullscreen} data-focus={workspace.specialFocus} style="--dir: {workspace.switchDirection}">
{#each ['board', 'agenda', 'calendar'] as const as tile (tile)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="window {tile}"
class:focused={workspace.specialFocus === tile}
onpointerdown={() => (workspace.specialFocus = tile)}
>
{#if tile === 'board'}
<div class="fill" bind:this={boardEl}><Board /></div>
{:else if tile === 'agenda'}
<Agenda />
{:else}
<Calendar />
{/if}
</div>
{/each}
</div> </div>
{:else} {:else}
<div <div
@@ -1218,7 +1275,7 @@
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex --> <!-- 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')} <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 if workspace.overlay === 'calendar'}<Calendar />{:else}<MediaBrowser />{/if} {#if workspace.overlay === 'help'}<Help />{:else if workspace.overlay === 'overview'}<Overview />{: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}
</div> </div>
{/if} {/if}
</div> </div>
@@ -1237,12 +1294,49 @@
<style> <style>
.boot { display: grid; place-items: center; height: 100%; } .boot { display: grid; place-items: center; height: 100%; }
.panes.board { grid-template-columns: 1fr; } /*
* The special workspace: the board takes what the right column leaves,
* the agenda takes what the calendar leaves under it.
*/
.panes.special {
grid-template-columns: minmax(0, 1fr) minmax(44ch, 30vw);
grid-template-rows: minmax(0, 1fr) auto;
grid-template-areas: 'board agenda' 'board calendar';
}
.special > .board { grid-area: board; }
.special > .agenda { grid-area: agenda; }
.special > .calendar { grid-area: calendar; }
.special .fill { height: 100%; }
/* 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);
color: var(--light);
}
.special > .window:not(.focused) :global(.row.at :is(.where, .due)) { color: var(--muted); }
.special > .window:not(.focused) :global(.card.active :is(.meta, .doc)) { color: var(--muted); }
.special > .window:not(.focused) :global(:is(.card.active .today, .today .row.at .due)) { color: var(--light); }
.special > .window:not(.focused) :global(:is(.card.active .overdue, .overdue .row.at .due)) { color: var(--alarm); }
.special > .window:not(.focused) :global(.card.active .tick) { border-color: var(--outline); }
.special > .window:not(.focused) :global(.day.at :is(.dot)) { background: var(--light); }
.special > .window:not(.focused) :global(.day.at .many) { color: var(--light); }
.panes.special.full { grid-template-columns: 1fr; grid-template-rows: 1fr; grid-template-areas: none; }
.panes.special.full > .window:not(.focused) { display: none; }
.panes.special.full > .window { grid-area: auto; }
/* The agenda and the calendar are sheets of their own as overlays; as tiles
the window is their sheet. */
.special > .window :global(.sheet) {
width: 100%;
height: 100%;
max-height: none;
border: 0;
border-radius: 0;
background: none;
}
.window.list { --pane-bg: color-mix(in oklab, var(--surface) 60%, var(--base)); } .window.list { --pane-bg: color-mix(in oklab, var(--surface) 60%, var(--base)); }
/* Only one pane on screen: nothing to tell apart, so no active border. */ /* Only one pane on screen: nothing to tell apart, so no active border. */
.panes:not(.reading) .window.list, .panes:not(.reading) .window.list {
.panes.board .window {
background: background:
linear-gradient(var(--pane-bg), var(--pane-bg)) padding-box, linear-gradient(var(--pane-bg), var(--pane-bg)) padding-box,
linear-gradient(var(--overlay), var(--overlay)) border-box; linear-gradient(var(--overlay), var(--overlay)) border-box;
@@ -1311,5 +1405,9 @@
.tiles > .window:not(.current) { display: none; } .tiles > .window:not(.current) { display: none; }
.tiles > .window.current { inset: calc(var(--gap) / 2); width: auto; height: auto; } .tiles > .window.current { inset: calc(var(--gap) / 2); width: auto; height: auto; }
.overlay { padding: 12px; } .overlay { padding: 12px; }
/* A phone shows the special workspace's focused tile alone. */
.panes.special { grid-template-columns: 1fr; grid-template-rows: 1fr; grid-template-areas: none; }
.panes.special > .window { grid-area: auto; }
.panes.special > .window:not(.focused) { display: none; }
} }
</style> </style>
+2 -2
View File
@@ -25,7 +25,7 @@
/** An entry from an earlier year, in place in the diary. */ /** An entry from an earlier year, in place in the diary. */
function open(entry: DiaryEntry) { function open(entry: DiaryEntry) {
workspace.overlay = 'none' workspace.closeSpecial()
void workspace.guard(() => workspace.openPath(entry.path)) void workspace.guard(() => workspace.openPath(entry.path))
} }
@@ -38,7 +38,7 @@
<div class="sheet" bind:this={sheet}> <div class="sheet" bind:this={sheet}>
<header> <header>
<span class="label rule"><span class="marker"></span><span class="title">agenda</span> {total} open</span> <span class="label rule"><span class="marker"></span><span class="title">agenda</span> {total} open</span>
<button onclick={() => (workspace.overlay = 'none')}>esc</button> <button onclick={() => workspace.closeSpecial()}>esc</button>
</header> </header>
{#if workspace.agenda.diary_today === false} {#if workspace.agenda.diary_today === false}
+2 -2
View File
@@ -40,8 +40,8 @@
<span class="divider path"></span> <span class="divider path"></span>
<nav class="crumbs" aria-label="path"> <nav class="crumbs" aria-label="path">
{#if workspace.view === 'board'} {#if workspace.view === 'special'}
<span class="here">board</span> <span class="here">special:{workspace.specialFocus}</span>
{:else} {:else}
<button <button
class="crumb" class="crumb"
+38 -3
View File
@@ -43,6 +43,11 @@
return index === 0 ? 'inbox' : index === lastIndex ? 'done' : '' return index === 0 ? 'inbox' : index === lastIndex ? 'done' : ''
} }
/** The spaces that have a board, with the digit that switches to each. */
const boards = $derived(
workspace.spaces.flatMap((space, index) => (space.has_board && index < 10 ? [{ space, digit: (index + 1) % 10 }] : [])),
)
function pick(list: number, card: number) { function pick(list: number, card: number) {
workspace.listCursor = list workspace.listCursor = list
workspace.cardCursor = card workspace.cardCursor = card
@@ -54,7 +59,18 @@
<span class="marker"></span> <span class="marker"></span>
<span class="name">{workspace.space?.slug ?? ''}</span> <span class="name">{workspace.space?.slug ?? ''}</span>
<span>board</span> <span>board</span>
<span class="count">{total} {total === 1 ? 'card' : 'cards'}</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
follows the space. -->
{#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))}>
<span class="n">{digit}</span>{space.slug}
</button>
{/each}
</span>
{/if}
</div> </div>
<div class="columns" bind:this={columns}> <div class="columns" bind:this={columns}>
@@ -116,7 +132,11 @@
</section> </section>
{:else} {:else}
<div class="empty muted"> <div class="empty muted">
no lists yet — :list &lt;name&gt; makes a column. then o adds a card to it. {#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}.
{:else}
no lists yet — :list &lt;name&gt; makes a column. then o adds a card to it.
{/if}
</div> </div>
{/each} {/each}
</div> </div>
@@ -131,7 +151,7 @@
<span><span class="kbd">D</span> due</span> <span><span class="kbd">D</span> due</span>
<span><span class="kbd">R</span> rename</span> <span><span class="kbd">R</span> rename</span>
<span><span class="kbd">dd</span> delete</span> <span><span class="kbd">dd</span> delete</span>
<span><span class="kbd">q</span> tree</span> <span><span class="kbd">1</span>…<span class="kbd">9</span> board</span>
<span class="rule-hint">first list is the inbox · last list is done</span> <span class="rule-hint">first list is the inbox · last list is done</span>
</footer> </footer>
</div> </div>
@@ -151,6 +171,21 @@
} }
.head .marker { background: var(--light); } .head .marker { background: var(--light); }
.head .name { color: var(--light); } .head .name { color: var(--light); }
.boards { display: flex; gap: 4px; margin-left: 8px; }
.other {
display: flex;
gap: 6px;
padding: 2px 8px;
border: 0;
border-radius: var(--radius-s);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: none;
color: var(--muted);
}
.other .n { color: var(--outline); }
.other.here { background: var(--surface-alt); color: var(--light); }
.other.here .n { color: var(--light-dim); }
.columns { .columns {
flex: 1; flex: 1;
+1 -1
View File
@@ -42,7 +42,7 @@
<span class="label rule"><span class="marker"></span><span class="title">{title}</span> {workspace.calendarEntries.length} entries</span> <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="previous month">‹</button>
<button onclick={() => workspace.guard(() => workspace.moveCalendar(0, 1))} aria-label="next month">›</button> <button onclick={() => workspace.guard(() => workspace.moveCalendar(0, 1))} aria-label="next month">›</button>
<button onclick={() => (workspace.overlay = 'none')}>esc</button> <button onclick={() => workspace.closeSpecial()}>esc</button>
</header> </header>
<ol class="grid"> <ol class="grid">
+8 -6
View File
@@ -72,12 +72,14 @@
['m / p', 'pick nodes up, then drop them all into the folder you walk to'], ['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'], ['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'], ['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'], ['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'],
['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"], ['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'], ['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'], ['[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'], ['Ctrl-P', 'find anything, in every space — Enter goes to the result lit'],
['b', "open this space's board"], ['b', "this space's board, in the special workspace — 1 … 9 there shows another space's"],
['c', "capture a card into this space's inbox, without leaving here"], ['c', "capture a card into this space's inbox, without leaving here"],
['Esc · q', 'close the document, and its tile when there are several'], ['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'], ['/', 'full-text search, inside the current space and its text files — Tab or Ctrl-N/P walk the results, Enter opens one'],
@@ -112,18 +114,18 @@
['F', 'a letter over every card in view — type it to put the cursor there'], ['F', 'a letter over every card in view — type it to put the cursor there'],
['r', 'read the board again, for changes made elsewhere'], ['r', 'read the board again, for changes made elsewhere'],
['x · dd', 'delete the card (asks y/N)'], ['x · dd', 'delete the card (asks y/N)'],
['q · Esc', 'back to the tree'], ['q · Esc', 'put the special workspace away'],
] ]
const agendaKeys: [string, string][] = [ 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'],
['Enter', 'to the card, on its own board — or to the entry'], ['Enter', 'to the card, on its own board beside — or to the entry'],
['Space', 'done, where it stands'], ['Space', 'done, where it stands'],
['D', 'due date: fri, tm, +3d … — :due on the line'], ['D', 'due date: fri, tm, +3d … — :due on the line'],
[':', 'the line, with the agenda kept up — :repeat weekly acts on the card'], [':', 'the line, with the agenda kept up — :repeat weekly acts on the card'],
['H / L', 'to the list left / right on its board'], ['H / L', 'to the list left / right on its board'],
['T', "today's diary entry"], ['T', "today's diary entry"],
['t · q · Esc', 'close the agenda'], ['t · q · Esc', 'put the special workspace away'],
] ]
const titles: Record<Group, string> = { const titles: Record<Group, string> = {
+12 -2
View File
@@ -30,15 +30,24 @@
const cmd: Door = { icon: 'prompt', label: 'Command line', keys: [':'], run: () => hooks.openCommandLine(':') } const cmd: Door = { icon: 'prompt', label: 'Command line', keys: [':'], run: () => hooks.openCommandLine(':') }
const help: Door = { icon: 'help', label: 'Help', keys: ['?'], run: () => runCommand('help') } const help: Door = { icon: 'help', label: 'Help', keys: ['?'], run: () => runCommand('help') }
if (workspace.view === 'board') { if (workspace.view === 'special') {
// The tiles of the special workspace are doors, so a phone — which
// shows one at a time — can walk between them.
const tiles: Door[] = [
{ 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() },
]
if (workspace.specialFocus !== 'board') return [tiles, [cmd, help]]
// The card verbs are doors too, so a phone — which has no H, L or // The card verbs are doors too, so a phone — which has no H, L or
// Space — can still move work along. // Space — can still move work along.
const card = !!workspace.card const card = !!workspace.card
return [ return [
tiles,
[ [
{ icon: 'doc-plus', label: 'New card on this list', keys: ['o'], run: () => hooks.openCommandLine(':card ') }, { icon: 'doc-plus', label: 'New card on this list', keys: ['o'], run: () => hooks.openCommandLine(':card ') },
{ icon: 'column-plus', label: 'New list', keys: [':list'], run: () => hooks.openCommandLine(':list ') }, { icon: 'column-plus', label: 'New list', keys: [':list'], run: () => hooks.openCommandLine(':list ') },
{ icon: 'list', label: 'Back to the tree', keys: ['b', 'q'], run: () => runCommand('board'), on: true },
], ],
[ [
{ icon: 'left', label: 'Send the card left', keys: ['H'], run: () => workspace.guard(() => workspace.sendCard(-1)), dim: !card }, { icon: 'left', label: 'Send the card left', keys: ['H'], run: () => workspace.guard(() => workspace.sendCard(-1)), dim: !card },
@@ -79,6 +88,7 @@
], ],
[ [
{ icon: 'agenda', label: 'Agenda — late, due, in hand', keys: ['t'], run: () => runCommand('agenda') }, { icon: 'agenda', label: 'Agenda — late, due, in hand', keys: ['t'], run: () => runCommand('agenda') },
{ icon: 'calendar', label: 'Calendar — the diary by month', keys: ['C'], run: () => runCommand('cal') },
{ icon: 'today', label: "Today's diary entry", keys: ['T'], run: () => runCommand('today') }, { icon: 'today', label: "Today's diary entry", keys: ['T'], run: () => runCommand('today') },
{ icon: 'capture', label: 'Capture a card to the inbox', keys: ['c'], run: () => hooks.openCommandLine(':card ') }, { icon: 'capture', label: 'Capture a card to the inbox', keys: ['c'], run: () => hooks.openCommandLine(':card ') },
], ],
+22 -17
View File
@@ -429,8 +429,14 @@ export const commands: CommandSpec[] = [
{ {
name: 'board', name: 'board',
group: 'board', group: 'board',
help: "show this space's board, or go back to the tree", help: "this space's board, in the special workspace — again puts it away",
run: () => workspace.guard(() => workspace.toggleBoardView()), run: () => workspace.guard(() => workspace.toggleSpecial('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',
run: () => workspace.guard(() => workspace.toggleSpecial()),
}, },
{ {
name: 'card', name: 'card',
@@ -447,7 +453,7 @@ export const commands: CommandSpec[] = [
run: async (arg) => { run: async (arg) => {
// On the board the tree is not on screen, so only the open document // On the board the tree is not on screen, so only the open document
// can be meant; elsewhere it is whatever has focus. // can be meant; elsewhere it is whatever has focus.
const node = workspace.view === 'board' ? workspace.open : workspace.target const node = workspace.view === 'special' ? workspace.open : workspace.target
if (!node || node.kind !== 'document') { if (!node || node.kind !== 'document') {
return workspace.say('no document to link — open one first', 'error') return workspace.say('no document to link — open one first', 'error')
} }
@@ -463,8 +469,8 @@ export const commands: CommandSpec[] = [
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', 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'], complete: () => ['today', 'tm', 'fri', '+3d'],
run: async (arg) => { run: async (arg) => {
const agenda = workspace.overlay === 'agenda' const agenda = workspace.view === 'special' && workspace.specialFocus === 'agenda'
if (workspace.view !== 'board' && !agenda) return workspace.say('no board open — :board', 'error') if (workspace.view !== 'special') return workspace.say('no board open — :board', 'error')
const set = (due_at: number | null) => const set = (due_at: number | null) =>
agenda ? workspace.patchAgendaCard({ due_at }) : workspace.patchCard({ due_at }) agenda ? workspace.patchAgendaCard({ due_at }) : workspace.patchCard({ due_at })
const target = arg.trim() const target = arg.trim()
@@ -482,8 +488,8 @@ export const commands: CommandSpec[] = [
help: 'once the card is done, a copy comes back in the inbox: daily, weekly, monthly, yearly, +3d, +2w, +6m — - stops it', help: 'once the card is done, a copy comes back in the inbox: daily, weekly, monthly, yearly, +3d, +2w, +6m — - stops it',
complete: () => ['daily', 'weekly', 'monthly', 'yearly', '+2w', '-'], complete: () => ['daily', 'weekly', 'monthly', 'yearly', '+2w', '-'],
run: async (arg) => { run: async (arg) => {
const agenda = workspace.overlay === 'agenda' const agenda = workspace.view === 'special' && workspace.specialFocus === 'agenda'
if (workspace.view !== 'board' && !agenda) return workspace.say('no board open — :board', 'error') if (workspace.view !== 'special') return workspace.say('no board open — :board', 'error')
const rule = arg.trim() const rule = arg.trim()
const repeat = rule === '-' || rule === '' ? null : rule const repeat = rule === '-' || rule === '' ? null : rule
await workspace.guard(async () => { await workspace.guard(async () => {
@@ -498,7 +504,7 @@ export const commands: CommandSpec[] = [
group: 'board', group: 'board',
help: 'move the selected card into the done list, or back out to the inbox', help: 'move the selected card into the done list, or back out to the inbox',
run: () => { run: () => {
if (workspace.view !== 'board') return workspace.say('no board open — :board', 'error') if (workspace.view !== 'special') return workspace.say('no board open — :board', 'error')
return workspace.guard(() => workspace.toggleDone()) return workspace.guard(() => workspace.toggleDone())
}, },
}, },
@@ -508,14 +514,14 @@ export const commands: CommandSpec[] = [
group: 'board', group: 'board',
help: 'add a column to the board', help: 'add a column to the board',
run: async (arg) => { run: async (arg) => {
if (workspace.view !== 'board') return workspace.say('no board open — :board', 'error') if (workspace.view !== 'special') return workspace.say('no board open — :board', 'error')
if (!arg.trim()) return workspace.say('usage: :list <name>', 'error') if (!arg.trim()) return workspace.say('usage: :list <name>', 'error')
await workspace.guard(() => workspace.createList(arg.trim())) await workspace.guard(() => workspace.createList(arg.trim()))
}, },
bang: { bang: {
help: 'delete the column under the cursor and every card on it', help: 'delete the column under the cursor and every card on it',
run: async () => { run: async () => {
if (workspace.view !== 'board') return workspace.say('no board open — :board', 'error') if (workspace.view !== 'special') return workspace.say('no board open — :board', 'error')
const list = workspace.list const list = workspace.list
if (!list) return workspace.say('no list selected', 'error') if (!list) return workspace.say('no list selected', 'error')
const cards = list.cards.length const cards = list.cards.length
@@ -593,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', 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: () => run: () =>
workspace.guard(async () => { workspace.guard(async () => {
if (workspace.overlay === 'calendar') return void (workspace.overlay = 'none') if (workspace.view === 'special') return workspace.toggleSpecial('calendar')
await workspace.openCalendar() await workspace.openCalendar()
}), }),
}, },
@@ -604,9 +610,8 @@ export const commands: CommandSpec[] = [
help: 'what is late, due this week, or in hand — across every board', help: 'what is late, due this week, or in hand — across every board',
run: () => run: () =>
workspace.guard(async () => { workspace.guard(async () => {
await workspace.loadAgenda() if (workspace.view !== 'special') workspace.agendaCursor = 0
workspace.agendaCursor = 0 await workspace.toggleSpecial('agenda')
workspace.overlay = workspace.overlay === 'agenda' ? 'none' : 'agenda'
}), }),
}, },
{ {
@@ -765,7 +770,7 @@ export const commands: CommandSpec[] = [
help: 'delete a node, and everything under it, into the trash (asks first) — u brings it back', help: 'delete a node, and everything under it, into the trash (asks first) — u brings it back',
run: async (arg) => { run: async (arg) => {
// On the board the thing under the cursor is a card, not a node. // On the board the thing under the cursor is a card, not a node.
if (workspace.view === 'board') { if (workspace.view === 'special') {
const card = workspace.card const card = workspace.card
if (!card) return workspace.say('no card selected', 'error') if (!card) return workspace.say('no card selected', 'error')
if (!(await workspace.ask(`delete the card "${card.title}"?`))) return if (!(await workspace.ask(`delete the card "${card.title}"?`))) return
@@ -793,7 +798,7 @@ export const commands: CommandSpec[] = [
help: 'rename what is under the cursor — document, folder, or card on a board; bare, while writing, into the title', help: 'rename what is under the cursor — document, folder, or card on a board; bare, while writing, into the title',
run: async (arg) => { run: async (arg) => {
if (!arg.trim() && workspace.editing && workspace.pane === 'document') return hooks.focusTitle() if (!arg.trim() && workspace.editing && workspace.pane === 'document') return hooks.focusTitle()
if (workspace.view === 'board') { if (workspace.view === 'special') {
if (!arg.trim()) return workspace.say('usage: :name <text>', 'error') if (!arg.trim()) return workspace.say('usage: :name <text>', 'error')
return workspace.guard(() => workspace.patchCard({ title: arg.trim() })) return workspace.guard(() => workspace.patchCard({ title: arg.trim() }))
} }
@@ -814,7 +819,7 @@ export const commands: CommandSpec[] = [
bang: { bang: {
help: 'rename the space you are in — on the board, the list under the cursor', help: 'rename the space you are in — on the board, the list under the cursor',
run: async (arg) => { run: async (arg) => {
if (workspace.view === 'board') { if (workspace.view === 'special') {
if (!arg.trim()) return workspace.say('usage: :name! <list name>', 'error') if (!arg.trim()) return workspace.say('usage: :name! <list name>', 'error')
return workspace.guard(() => workspace.renameList(arg.trim())) return workspace.guard(() => workspace.renameList(arg.trim()))
} }
+58 -38
View File
@@ -23,8 +23,14 @@ import {
import { formatBytes, formatDay, formatStamp, formatWeekday, isTouchDevice, parseDay, sealedNote, type Heading } from './util' import { formatBytes, formatDay, formatStamp, formatWeekday, isTouchDevice, parseDay, sealedNote, type Heading } from './util'
export type Pane = 'list' | 'document' export type Pane = 'list' | 'document'
/** The tree of documents, or the board of the space being browsed. */ /**
export type View = 'tree' | 'board' * The tree of documents, or the special workspace: the board of the space
* being browsed, the agenda and the calendar, tiled side by side and a key
* away from anywhere, like the desktop's scratchpad.
*/
export type View = 'tree' | 'special'
/** The tile of the special workspace that has focus. */
export type SpecialTile = 'board' | 'agenda' | 'calendar'
export type Flash = { text: string; kind: 'info' | 'error' } | null export type Flash = { text: string; kind: 'info' | 'error' } | null
/** /**
@@ -290,7 +296,9 @@ class Workspace {
pane = $state<Pane>('list') pane = $state<Pane>('list')
flash = $state<Flash>(null) flash = $state<Flash>(null)
overlay = $state<'none' | 'help' | 'media' | 'overview' | 'agenda' | 'actions' | 'verify' | 'outline' | 'trash' | 'history' | 'backlinks' | 'calendar'>('none') 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')
/** `gO`: the headings of what is on screen, to jump between from the keyboard. */ /** `gO`: the headings of what is on screen, to jump between from the keyboard. */
outline = $state.raw<Heading[]>([]) outline = $state.raw<Heading[]>([])
outlineCursor = $state(0) outlineCursor = $state(0)
@@ -813,14 +821,14 @@ class Workspace {
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))
} }
} }
// The board belongs to the space, so switching space while looking at one // The board belongs to the space, so switching space with the special
// shows the new space's board — or drops back to the tree for a space that // workspace up shows the new space's board — or says it has none, rather
// has none, rather than leaving the previous board on screen. // than leaving the previous board on screen.
if (this.view === 'board') { if (this.view === 'special') {
this.listCursor = 0 this.listCursor = 0
this.cardCursor = 0 this.cardCursor = 0
if (this.space?.has_board) await this.reloadBoard(id, undefined) if (this.space?.has_board) await this.reloadBoard(id, undefined)
else this.closeBoard() else this.rawBoard = null
} }
} }
@@ -862,7 +870,7 @@ 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. // The results are rows in the list, which the board would hide.
if (query && this.view === 'board') this.closeBoard() if (query && this.view === 'special') this.closeSpecial()
this.query = query this.query = query
this.globalSearch = global && query !== '' this.globalSearch = global && query !== ''
this.cursor = 0 this.cursor = 0
@@ -898,8 +906,8 @@ class Workspace {
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 // 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. // opened behind it; closing a document goes back to the board.
if (this.view === 'board' && !underBoard) { if (this.view === 'special' && !underBoard) {
if (isContainer(node)) this.closeBoard() if (isContainer(node)) this.closeSpecial()
else { else {
this.view = 'tree' this.view = 'tree'
this.returnTo = 'board' this.returnTo = 'board'
@@ -1314,7 +1322,7 @@ class Workspace {
if (late + due === 0) return if (late + due === 0) return
localStorage.setItem('diary:agenda-seen', today) localStorage.setItem('diary:agenda-seen', today)
this.agendaCursor = 0 this.agendaCursor = 0
this.overlay = 'agenda' await this.openSpecial('agenda')
} }
/** `C`: the calendar, on the day of the entry being read or else today. */ /** `C`: the calendar, on the day of the entry being read or else today. */
@@ -1322,8 +1330,7 @@ class Workspace {
const open = this.open const open = this.open
const reading = open && this.spaces.find((space) => space.id === open.path[0]?.id)?.role === 'diary' 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)) this.calendarDay = formatDay(reading ? open.created_at : Math.floor(Date.now() / 1000))
await this.loadCalendar() await this.openSpecial('calendar')
this.overlay = 'calendar'
} }
async loadCalendar() { async loadCalendar() {
@@ -1344,7 +1351,7 @@ class Workspace {
async openCalendarDay() { async openCalendarDay() {
const day = this.calendarDay const day = this.calendarDay
const entry = this.calendarEntries.find((e) => e.day === day) const entry = this.calendarEntries.find((e) => e.day === day)
this.overlay = 'none' this.closeSpecial()
if (entry) return this.openPath(entry.path) if (entry) return this.openPath(entry.path)
const home = this.spaces.find((space) => space.role === 'diary') const home = this.spaces.find((space) => space.role === 'diary')
if (!home) return this.say('there is no diary to write in', 'error') if (!home) return this.say('there is no diary to write in', 'error')
@@ -1363,12 +1370,12 @@ class Workspace {
this.say(entry.day) this.say(entry.day)
} }
/** Enter in the agenda: to the card, on its own board. */ /** Enter in the agenda: to the card, on its own board, beside the agenda. */
async goToCard(card: AgendaCard) { async goToCard(card: AgendaCard) {
this.overlay = 'none'
if (this.spaceId !== card.space_id) await this.enterSpace(card.space_id) if (this.spaceId !== card.space_id) await this.enterSpace(card.space_id)
await this.reloadBoard(card.space_id, card.id) await this.reloadBoard(card.space_id, card.id)
this.view = 'board' this.view = 'special'
this.specialFocus = 'board'
} }
/** Space in the agenda: into that board's done list, from wherever it is. */ /** Space in the agenda: into that board's done list, from wherever it is. */
@@ -1377,7 +1384,7 @@ class Workspace {
if (!done) return if (!done) return
const moved = await api.moveCard(card.id, done.id, done.cards.length) const moved = await api.moveCard(card.id, done.id, done.cards.length)
await this.loadAgenda() await this.loadAgenda()
if (this.view === 'board' && this.spaceId === card.space_id) await this.reloadBoard() if (this.view === 'special' && this.spaceId === card.space_id) await this.reloadBoard()
this.say(doneSaid(moved)) this.say(doneSaid(moved))
} }
@@ -1390,7 +1397,7 @@ class Workspace {
// It may have changed group; the cursor goes with it. // It may have changed group; the cursor goes with it.
const at = this.agendaCards.findIndex((c) => c.id === card.id) const at = this.agendaCards.findIndex((c) => c.id === card.id)
if (at >= 0) this.agendaCursor = at if (at >= 0) this.agendaCursor = at
if (this.view === 'board' && this.spaceId === card.space_id) await this.reloadBoard() if (this.view === 'special' && this.spaceId === card.space_id) await this.reloadBoard()
return updated return updated
} }
@@ -1407,7 +1414,7 @@ class Workspace {
await this.loadAgenda() await this.loadAgenda()
const at = this.agendaCards.findIndex((c) => c.id === card.id) const at = this.agendaCards.findIndex((c) => c.id === card.id)
if (at >= 0) this.agendaCursor = at if (at >= 0) this.agendaCursor = at
if (this.view === 'board' && this.spaceId === card.space_id) await this.reloadBoard() if (this.view === 'special' && this.spaceId === card.space_id) await this.reloadBoard()
this.say(from + delta === lists.length - 1 ? doneSaid(moved) : `"${card.title}" → ${target.name}`) this.say(from + delta === lists.length - 1 ? doneSaid(moved) : `"${card.title}" → ${target.name}`)
} }
@@ -1475,24 +1482,40 @@ class Workspace {
* where a card is would be worse than a round trip. * where a card is would be worse than a round trip.
*/ */
async openBoard() { /**
* The special workspace, with `focus` the tile the keys go to. Everything
* in it is read again on the way in: it is looked at in passing, and what
* it says should be what is true now.
*/
async openSpecial(focus: SpecialTile = this.specialFocus) {
const space = this.space const space = this.space
if (!space) return if (space?.has_board) await this.reloadBoard(space.id)
if (!space.has_board) { else this.rawBoard = null
return this.say(`${space.slug} has no board — :set board turns one on`, 'error') if (!this.calendarDay) this.calendarDay = formatDay(Math.floor(Date.now() / 1000))
} await Promise.all([this.loadAgenda(), this.loadCalendar()])
await this.reloadBoard(space.id) this.view = 'special'
this.view = 'board' this.specialFocus = focus
if (focus === 'board' && space && !space.has_board) this.say(`${space.slug} has no board — :set board turns one on`, 'error')
} }
closeBoard() { closeSpecial() {
this.view = 'tree' this.view = 'tree'
this.returnTo = null this.returnTo = null
} }
async toggleBoardView() { /**
if (this.view === 'board') this.closeBoard() * `b`, `t`, `C` and `S`: the special workspace on that tile. A key whose
else await this.openBoard() * tile already has focus puts the workspace away again, as the desktop
* toggles its scratchpad; any other moves the focus there.
*/
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 openBoard() {
await this.openSpecial('board')
} }
async reloadBoard(spaceId = this.spaceId, keep = this.card?.id) { async reloadBoard(spaceId = this.spaceId, keep = this.card?.id) {
@@ -1582,7 +1605,7 @@ class Workspace {
* it goes on the list under the cursor instead. * it goes on the list under the cursor instead.
*/ */
async createCard(title: string, nodeId: number | null = null) { async createCard(title: string, nodeId: number | null = null) {
if (this.view === 'board') { if (this.view === 'special') {
const list = this.list const list = this.list
if (!list) return this.say('no list to put it on', 'error') if (!list) return this.say('no list to put it on', 'error')
const card = await api.createCard(list.id, { title, node_id: nodeId }) const card = await api.createCard(list.id, { title, node_id: nodeId })
@@ -1704,7 +1727,7 @@ class Workspace {
this.closeTile() this.closeTile()
this.pane = 'list' this.pane = 'list'
await this.reloadBoard() await this.reloadBoard()
this.view = 'board' this.view = 'special'
} }
/** Turn a space's board on or off. The diary ships with it off. */ /** Turn a space's board on or off. The diary ships with it off. */
@@ -1714,10 +1737,7 @@ class Workspace {
await api.updateNode(space.id, { has_board: on }) await api.updateNode(space.id, { has_board: on })
this.spaces = await api.listSpaces() this.spaces = await api.listSpaces()
if (on) await this.openBoard() if (on) await this.openBoard()
else { else this.rawBoard = null
this.closeBoard()
this.rawBoard = null
}
this.say(on ? `${space.slug} has a board` : `${space.slug} has no board any more`) this.say(on ? `${space.slug} has a board` : `${space.slug} has no board any more`)
} }