diff --git a/README.md b/README.md index f62f80c..d961748 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/wiki/Boards-and-Agenda.md b/docs/wiki/Boards-and-Agenda.md index 67de9ba..b038e28 100644 --- a/docs/wiki/Boards-and-Agenda.md +++ b/docs/wiki/Boards-and-Agenda.md @@ -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 diff --git a/docs/wiki/Commands.md b/docs/wiki/Commands.md index 651e607..9a9a4ad 100644 --- a/docs/wiki/Commands.md +++ b/docs/wiki/Commands.md @@ -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! `, `: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 ` | 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 | diff --git a/docs/wiki/Diary.md b/docs/wiki/Diary.md index 4f740de..9c2fcc3 100644 --- a/docs/wiki/Diary.md +++ b/docs/wiki/Diary.md @@ -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. diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index cbaba9b..9598a7c 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -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. diff --git a/docs/wiki/Guide.md b/docs/wiki/Guide.md index aa2fb2b..e7517e4 100644 --- a/docs/wiki/Guide.md +++ b/docs/wiki/Guide.md @@ -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 diff --git a/web/src/App.svelte b/web/src/App.svelte index 43941cc..58f8db3 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -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); diff --git a/web/src/lib/Agenda.svelte b/web/src/lib/Agenda.svelte index c55b407..e0183a9 100644 --- a/web/src/lib/Agenda.svelte +++ b/web/src/lib/Agenda.svelte @@ -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> diff --git a/web/src/lib/Bar.svelte b/web/src/lib/Bar.svelte index 43306e5..bd24a4a 100644 --- a/web/src/lib/Bar.svelte +++ b/web/src/lib/Bar.svelte @@ -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. */ diff --git a/web/src/lib/Board.svelte b/web/src/lib/Board.svelte index 90aff30..7721849 100644 --- a/web/src/lib/Board.svelte +++ b/web/src/lib/Board.svelte @@ -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 <name> 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; diff --git a/web/src/lib/Calendar.svelte b/web/src/lib/Calendar.svelte index 54ff604..8db74b2 100644 --- a/web/src/lib/Calendar.svelte +++ b/web/src/lib/Calendar.svelte @@ -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> diff --git a/web/src/lib/Help.svelte b/web/src/lib/Help.svelte index fc62a5d..69145d0 100644 --- a/web/src/lib/Help.svelte +++ b/web/src/lib/Help.svelte @@ -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> = { diff --git a/web/src/lib/Rail.svelte b/web/src/lib/Rail.svelte index df4e8db..2367253 100644 --- a/web/src/lib/Rail.svelte +++ b/web/src/lib/Rail.svelte @@ -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 diff --git a/web/src/lib/commands.ts b/web/src/lib/commands.ts index aa52b0d..2f14acf 100644 --- a/web/src/lib/commands.ts +++ b/web/src/lib/commands.ts @@ -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') }), }, { diff --git a/web/src/lib/store.svelte.ts b/web/src/lib/store.svelte.ts index 1ea1490..7ca4ac8 100644 --- a/web/src/lib/store.svelte.ts +++ b/web/src/lib/store.svelte.ts @@ -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`) }