keys: Ctrl moves between tiles, Ctrl-Q jots from anywhere, the plan gets a screen of its own

Alt never reached the app, so the tile keys are Ctrl-h/j/k/l and
Ctrl-Shift-h/j/k/l, taken before vim sees them, and h/l walk the tiles
while reading. Ctrl-Q lowers the scratchpad from the editor straight
into the last note, and raises it back to the document, still writing.
The drawer sits in the porthole's opening with a full rim. The board,
the agenda, the calendar and a card's back live on a plan screen that
S goes to and comes back from, so documents and views never share the
screen. The calendar fits its tile instead of scrolling.
This commit is contained in:
nvrl committed 2026-10-22 16:51:50 +02:00
1 parent b1107013ee
commit 650fc40fa7
26 files changed
+340 -221

No files matched your search

+25 -15
View File
@@ -118,11 +118,12 @@ it; in the document pane it is that document. In the browse pane:
| `Tab`, `Shift-Tab` | next / previous space | | `Tab`, `Shift-Tab` | next / previous space |
| `w` | overview of every space; `hjkl` then `Enter` | | `w` | overview of every space; `hjkl` then `Enter` |
| `f` | fullscreen the focused pane, or the focused tile | | `f` | fullscreen the focused pane, or the focused tile |
| `Alt-h/j/k/l` | focus the tile in that direction, from the editor too; `Alt-h` past the leftmost tile focuses the list | | `Ctrl-h/j/k/l` | focus the tile in that direction, from the editor too; `Ctrl-h` past the leftmost tile focuses the list |
| `Alt-Shift-h/j/k/l` | move the focused tile that way | | `h` / `l` | while reading, the tile to the left / right; `h` past the leftmost goes to the list |
| `Ctrl-Shift-h/j/k/l` | move the focused tile that way |
| `L`, `Shift-Enter` | open the highlighted row in a new tile instead of the focused one | | `L`, `Shift-Enter` | open the highlighted row in a new tile instead of the focused one |
| `gt` / `gT` | next / previous tile, in the editor too | | `gt` / `gT` | next / previous tile, in the editor too |
| `Alt-q` | close the focused tile (`:close`); asks first if its draft could not be written | | `q` | close the document, and its tile when there are several (`:close`); asks first if its draft could not be written |
| `j` / `k` | move down / up, or scroll the document when it has focus | | `j` / `k` | move down / up, or scroll the document when it has focus |
| `gg` / `G` | first / last entry, or top / bottom of the document; a first `g`, `d`, `]` or `[` shows in the status line for a second while it waits for the second key | | `gg` / `G` | first / last entry, or top / bottom of the document; a first `g`, `d`, `]` or `[` shows in the status line for a second while it waits for the second key |
| `Ctrl-D` / `Ctrl-U` | half a page down / up the open document | | `Ctrl-D` / `Ctrl-U` | half a page down / up the open document |
@@ -143,14 +144,15 @@ it; in the document pane it is that document. In the browse pane:
| `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 |
| `za` | fold a document's own files open under it in the list, or away again | | `za` | fold a document's own files open under it in the list, or away again |
| `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 |
| `S` | the board, the agenda and the calendar tiled ahead of what is open, or every view away again; `Alt-h`/`j`/`k`/`l` move between the tiles, `f` gives one the screen | | `S` | the plan screen — the board, the agenda and the calendar, in place of the documents and the list — or back to the documents as they were left |
| `b` | the board of this space, in a tile; a digit there shows another space's | | `b` | the board of this space, on the plan screen; a digit there shows another space's |
| `t` | the agenda: what is late, due this week, or in hand, on every board, in a tile | | `t` | the agenda: what is late, due this week, or in hand, on every board, on the plan screen |
| `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 |
| `c` | capture a card into this space's inbox without leaving where you are — reading a document, a card about it; `Tab` on the line makes it a note, or a line in today's entry | | `c` | capture a card into this space's inbox without leaving where you are — reading a document, a card about it; `Tab` on the line makes it a note, or a line in today's entry |
| `Q` | the scratchpad, lowered over whatever is on screen — see [The scratchpad](docs/wiki/Writing.md#the-scratchpad) | | `Q` | the scratchpad, lowered over whatever is on screen — see [The scratchpad](docs/wiki/Writing.md#the-scratchpad) |
| `Ctrl-Q` | from anywhere, the editor too: the scratchpad down with the last note being written, and again back to where you were, still writing (Firefox quits on it instead) |
| `Esc`, `q` | close the document, and its tile when there are several; `Esc` first puts down what `m` picked up | | `Esc`, `q` | close the document, and its tile when there are several; `Esc` first puts down what `m` picked up |
| `h` | up one level in the tree | | `h` | up one level in the tree |
| `/` | full-text search in this space (`:noh` clears it); `Tab` or `Ctrl-N`/`Ctrl-P` walk the results without leaving the line, `Enter` opens the one lit | | `/` | full-text search in this space (`:noh` clears it); `Tab` or `Ctrl-N`/`Ctrl-P` walk the results without leaving the line, `Enter` opens the one lit |
@@ -222,13 +224,21 @@ its own scroll position and its own editor, and the keys always act on the
focused one. Opening a document that is already in a tile focuses that tile focused one. Opening a document that is already in a tile focuses that tile
rather than opening it twice. rather than opening it twice.
`Alt-h/j/k/l` moves the focus between tiles and `Alt-Shift-h/j/k/l` moves the `Ctrl-h/j/k/l` moves the focus between tiles, from the editor too, and
tile itself; `gt` / `gT` and `:bn` / `:bp` cycle through them, `:ls` lists `Ctrl-Shift-h/j/k/l` moves the tile itself; while reading, `h` and `l` go to
them, `:b <n or name>` focuses one, `:close` (`Alt-q`) closes one and `:only` the tile left and right. `gt` / `gT` and `:bn` / `:bp` cycle through them,
every other. `:next` and `:prev` still walk the list. Each space remembers its `:ls` lists them, `:b <n or name>` focuses one, `:close` (`q` while reading)
tiles, so switching away and back finds them as they were. A card's notes closes one and `:only` every other. `:next` and `:prev` still walk the list.
open in a tile beside what was being read, and `q` there goes back to the Each space remembers its tiles, so switching away and back finds them as they
board. Under 768px wide only the focused tile is shown. were. Under 768px wide only the focused tile is shown.
The views — the board, the agenda, the calendar and a card's back — have a
screen of their own, the plan, which `S` goes to and comes back from; `b`, `t`
and `C` go there too, to their own view. The documents are put away behind it
as they are, and the plan stays as it is from space to space. Opening a
document from the plan — a card's notes, a day of the calendar — goes back to
the documents and opens it beside them; `q` on a view does the same, and on a
card's back goes back to the board.
## The SFTP mount ## The SFTP mount
@@ -257,8 +267,8 @@ start as `sftp_host_key` in the data directory.
## Boards ## Boards
`b`, the board door on the rail, or `:board` shows the board of the space `b`, the board door on the rail, or `:board` shows the board of the space
being browsed in a tile beside what is open, and `q` or `Esc` closes it; `S` being browsed on the plan screen, beside the agenda and the calendar, and `q`
puts it up with the agenda and the calendar. Every space has one except the or `Esc` goes back to the documents. 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
+23 -26
View File
@@ -3,42 +3,39 @@
## Views ## Views
The board, the agenda, the [calendar](Diary.md#the-calendar) and the back of The board, the agenda, the [calendar](Diary.md#the-calendar) and the back of
a card are views: tiles like the documents' tiles, opened beside what is on a card are views, and they have a screen of their own: the plan. `S` (or
screen and walked the same way. `S` (or `:plan`) puts the board, the agenda `:plan`) goes there — the board on the left, the agenda over the calendar on
and the calendar up together, ahead of the documents with the board focused — the right, and no list — and back to the documents, which wait behind it as
the board on the left, the agenda over the calendar on the right when nothing they were left. The views are tiles like the documents', walked the same way.
else is open — and puts every view away again.
![The board, the agenda and the calendar](../images/board.png) ![The board, the agenda and the calendar](../images/board.png)
| key | what it does | | key | what it does |
| --- | --- | | --- | --- |
| `S` | the board, the agenda and the calendar up together, or every view away | | `S` | to the plan screen, or back to the documents |
| `b`, `t`, `C` | to the board, the agenda or the calendar, opening its tile when it is not up | | `b`, `t`, `C` | to the board, the agenda or the calendar, on the plan screen |
| `gt` / `gT` | the next / previous tile | | `gt` / `gT` | the next / previous tile |
| `Alt-h` / `Alt-j` / `Alt-k` / `Alt-l` | the tile that way; left of the leftmost is the list | | `Ctrl-h` / `Ctrl-j` / `Ctrl-k` / `Ctrl-l` | the tile that way |
| `l` past the last list, `h` in the agenda, `j` past its end | on into the tile beside | | `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 | | `f` | the focused tile fills the screen; again or `Esc` to leave |
| `1` … `9` | switch space; with a view focused, the views come along | | `1` … `9` | switch space; the plan stays, and the board shows that space's |
| `q`, `Esc` | close the view's tile, back to the tile focused before it | | `q`, `Esc` | from a card's back to the board; from the board, the agenda or the calendar back to the documents |
The keys go to the tile with focus, which wears the light border and its key The keys go to the tile with focus, which wears the light border and its key
lit in its head; the other views keep their cursors, dimmed. A view is never lit in its head; the other views keep their cursors, dimmed. What is opened
replaced by what is opened from it — a card's notes, a day of the calendar, an from a view — a card's notes, a day of the calendar, an entry of the agenda —
entry of the agenda, a row of the list: that opens in a tile beside it, and is a document, so it opens on the documents' screen, beside what is there, and
`q` on it comes back to the view. Each view is read again whenever it is gone `S` comes back to the plan. Each view is read again whenever it is gone to.
to.
Each space keeps its own tiles, views among them. Switching space with a view The plan is the same in every space, the way the desktop's special workspace
focused takes the board, the agenda and the calendar along, the way the stays up: a digit on the board shows that space's board, with the agenda still
special workspace used to stay up: a digit on the board shows that space's beside it. Each space keeps its own documents behind it, and they are there
board, with the agenda still beside it. Switching with a document or the list again on going back.
focused leaves them in the space, and they are there again on coming back.
## 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` shows it in a tile beside what is open, and `q` or `Esc` closes it. `:board` shows it on the plan screen, and `q` or `Esc` goes back to the documents.
It is the board of the space being browsed, or, in a space without one such as 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 the diary, the last board shown. The head of the board names every space that
has one, with the digit that goes there. has one, with the digit that goes there.
@@ -119,8 +116,8 @@ outside the browser, see [Capture](Capture.md).
### Notes ### Notes
`gf` on a card, or on its back, opens its notes: a document named after the card, made in the `gf` on a card, or on its back, opens its notes: a document named after the card, made in the
root of the space the first time, in a tile beside the board. Closing it goes root of the space the first time, among the documents. `S` goes back to the
back to the board, on the same card. board, on the same card.
The link is loose on purpose: deleting the document leaves the card, because the The link is loose on purpose: deleting the document leaves the card, because the
work is still open when its notes are gone. work is still open when its notes are gone.
@@ -186,8 +183,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, in a tile `t` (or `:agenda`) gathers what needs you from every board at once, on the
of its own. plan screen.
![The agenda](../images/agenda.png) ![The agenda](../images/agenda.png)
@@ -201,7 +198,7 @@ of its own.
| --- | --- | | --- | --- |
| `j` / `k` | down / up, on into the diary entries, then into the tile below | | `j` / `k` | down / up, on into the diary entries, then into the tile below |
| `h` | to the tile on the left | | `h` | to the tile on the left |
| `Enter` | to the card on its own board, in the tile beside, or open the entry | | `Enter` | to the card on its own board, beside, or open the entry among the documents |
| `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 |
+13 -13
View File
@@ -19,11 +19,10 @@ arguments and `↑` / `↓` recall earlier lines.
| `Tab · Shift-Tab` | next / previous space | | `Tab · Shift-Tab` | next / previous space |
| `w` | overview of every space — hjkl, then Enter | | `w` | overview of every space — hjkl, then Enter |
| `f` | fullscreen the focused pane or tile; again or Esc to leave | | `f` | fullscreen the focused pane or tile; again or Esc to leave |
| `Alt-h/j/k/l` | focus the tile that way, from the editor too — left of the leftmost is the list | | `Ctrl-h/j/k/l` | focus the tile that way, from the editor too — left of the leftmost is the list; h / l do the same while reading |
| `Alt-Shift-h/j/k/l` | move the focused tile that way | | `Ctrl-Shift-h/j/k/l` | move the focused tile that way |
| `L · Shift-Enter` | open the row in a new tile beside the others | | `L · Shift-Enter` | open the row in a new tile beside the others |
| `gt / gT` | next / previous tile — a view too | | `gt / gT` | next / previous tile — a view too |
| `Alt-q` | close the focused tile — :close; :only closes the others |
| `j / k · ↓ ↑` | move down / up the list — scroll the document when it has focus | | `j / k · ↓ ↑` | move down / up the list — scroll the document when it has focus |
| `gg / G` | first / last row, or top / bottom of the document — a first g, d, ] or [ waits a second in the status line | | `gg / G` | first / last row, or top / bottom of the document — a first g, d, ] or [ waits a second in the status line |
| `Ctrl-D / Ctrl-U` | half a page down / up the open document | | `Ctrl-D / Ctrl-U` | half a page down / up the open document |
@@ -44,13 +43,13 @@ arguments and `↑` / `↓` recall earlier lines.
| `za` | a document's own files, folded open under it in the list — again folds them away; p drops marked files in there | | `za` | a document's own files, folded open under it in the list — again folds them away; p drops marked files in there |
| `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 |
| `S` | the board, the agenda and the calendar, tiled ahead of what is open with the board focused — S again puts every view away | | `S` | the plan screen: the board, the agenda and the calendar, in place of the documents and the list — S again goes back to the documents as they were |
| `t` | agenda — late, due this week, in hand, on every board — in a tile of its own | | `t` | agenda — late, due this week, in hand, on every board — on the plan screen |
| `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, in a tile: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day beside it | | `C` | the diary by month, on the plan screen: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day among the documents |
| `[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 opens the result lit beside them, n goes on | | `Ctrl-P` | find anything, in every space — Enter opens the result lit beside them, n goes on |
| `b` | the board, in a tile: this space's, or the last one shown — 1 … 9 there shows another's | | `b` | the board, on the plan screen: 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 — reading a document, a card about it; Tab on the line makes it a note, or a line in today's entry | | `c` | capture a card into this space's inbox, without leaving here — reading a document, a card about it; Tab on the line makes it a note, or a line in today's entry |
| `Esc · q` | close the document, and its tile when there are several — Esc first puts down what m picked up | | `Esc · q` | close the document, and its tile when there are several — Esc first puts down what m picked up |
| `/` | 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 |
@@ -64,7 +63,7 @@ arguments and `↑` / `↓` recall earlier lines.
| `Ctrl-S` | save, from anywhere | | `Ctrl-S` | save, from anywhere |
| `?` | this screen — the keys to start with, then on a second ? every key and command; h/l a column, / filters it down | | `?` | this screen — the keys to start with, then on a second ? every key and command; h/l a column, / filters it down |
| `,` | the settings sheet: vim and the clipboard, the vault, the backup, signed-in browsers, ssh keys, capture tokens | | `,` | the settings sheet: vim and the clipboard, the vault, the backup, signed-in browsers, ssh keys, capture tokens |
| `q · Esc` | in a view — the board, the agenda, the calendar, a card's back — close its tile; the focus goes back to the tile before | | `q · Esc` | on the plan screen: from a card's back to the board, from the board, the agenda or the calendar back to the documents |
### On a board ### On a board
@@ -79,7 +78,7 @@ The first list is the inbox, the last list is done.
| `H / L` | move the card to the list left / right | | `H / L` | move the card to the list left / right |
| `Space` | into the done list, or back out to the inbox | | `Space` | into the done list, or back out to the inbox |
| `Enter` | turn the card over, in a tile beside: its labels, description and checklist | | `Enter` | turn the card over, in a tile beside: its labels, description and checklist |
| `gf` | the card's notes — a document, made on first use; q comes back | | `gf` | the card's notes — a document, made on first use, opened among the documents; S comes back |
| `o · c` | new card on this list | | `o · c` | new card on this list |
| `D` | due date: fri, tm, +3d, 10-31 … | | `D` | due date: fri, tm, +3d, 10-31 … |
| `R` | rename the card, or the list when it is empty | | `R` | rename the card, or the list when it is empty |
@@ -103,7 +102,7 @@ The first list is the inbox, the last list is done.
| `a` | put on a label, or take it off — :label on the line, Tab offers the board's | | `a` | put on a label, or take it off — :label on the line, Tab offers the board's |
| `R · D` | rename the card, its due date | | `R · D` | rename the card, its due date |
| `H / L` | send the card to the list left / right | | `H / L` | send the card to the list left / right |
| `gf` | the card's notes — a document, made on first use; q comes back | | `gf` | the card's notes — a document, made on first use, opened among the documents; S comes back |
### In the agenda ### In the agenda
@@ -123,6 +122,7 @@ The first list is the inbox, the last list is done.
| key | what it does | | key | what it does |
| --- | --- | | --- | --- |
| `Q` | lower the scratchpad over whatever is on screen, or raise it again — :scratch <text> takes a note without it | | `Q` | lower the scratchpad over whatever is on screen, or raise it again — :scratch <text> takes a note without it |
| `Ctrl-Q` | from anywhere, the editor too: the scratchpad down with the last note being written, and again back to where you were, still writing — Firefox quits on it instead |
| `j / k` | down / up the notes, the one touched last on top | | `j / k` | down / up the notes, the one touched last on top |
| `o` | a new note, written straight away — it saves itself | | `o` | a new note, written straight away — it saves itself |
| `Enter · e · i` | write the note — Esc in normal mode, or :q, comes back to the notes | | `Enter · e · i` | write the note — Esc in normal mode, or :q, comes back to the notes |
@@ -154,8 +154,8 @@ whole workspace. A command that does a second thing takes a word for it:
| command | what it does | | command | what it does |
| --- | --- | | --- | --- |
| `:board` | this space's board, in a tile — the last one shown when this space has none | | `:board` | this space's board, on the plan screen — the last one shown when this space has none |
| `:plan, :special` | the board, the agenda and the calendar, tiled ahead of what is open — S puts them up, and every view away again; 1 … 9 on the board picks the board | | `:plan, :special` | the plan screen: the board, the agenda and the calendar, in place of the documents — S goes there and back again; 1 … 9 on the board picks the board |
| `:card, :c [title]` | add a card — reading a document, one about it, titled after it unless named, and gf on it comes back; on the board to the list under the cursor, elsewhere to this space's inbox | | `:card, :c [title]` | add a card — reading a document, one about it, titled after it unless named, and gf on it comes back; on the board to the list under the cursor, elsewhere to this space's inbox |
| `:due <when\|->` | due date of the card in focus — on the board, its back, in the agenda, or the open document's: today, tm, fri, +3d, +2w, 10-31, 2026-10-31 — - clears | | `:due <when\|->` | due date of the card in focus — on the board, its back, in the agenda, or the open document's: today, tm, fri, +3d, +2w, 10-31, 2026-10-31 — - clears |
| `:repeat <rule\|->` | once the card is done, a copy comes back in the inbox: daily, weekly, monthly, yearly, +3d, +2w, +6m — - stops it | | `:repeat <rule\|->` | once the card is done, a copy comes back in the inbox: daily, weekly, monthly, yearly, +3d, +2w, +6m — - stops it |
@@ -196,7 +196,7 @@ whole workspace. A command that does a second thing takes a word for it:
| command | what it does | | command | what it does |
| --- | --- | | --- | --- |
| `:close, :clo` | close the focused tile — Alt-q; asks when its draft could not be written | | `:close, :clo` | close the focused tile — q while reading; on the plan screen, back to the documents; asks when its draft could not be written |
| `:only, :on` | close every tile but the focused one | | `:only, :on` | close every tile but the focused one |
| `:ls, :buffers, :files` | list the open tiles, % on the focused one | | `:ls, :buffers, :files` | list the open tiles, % on the focused one |
| `:buffer, :b <n\|name>` | focus a tile by its number in :ls, or by the start of its name | | `:buffer, :b <n\|name>` | focus a tile by its number in :ls, or by the start of its name |
+5 -5
View File
@@ -13,9 +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, in a `C` (or `:cal`) shows the diary a month at a time, on the
[tile of its own](Boards-and-Agenda.md#views) — under the agenda when `S` puts [plan screen](Boards-and-Agenda.md#views), under the agenda. The month is
them up together. fitted into its tile rather than scrolled.
![The diary by month](../images/calendar.png) ![The diary by month](../images/calendar.png)
@@ -25,8 +25,8 @@ them up together.
| `j` / `k` | a week forward / back | | `j` / `k` | a week forward / back |
| `H` / `L` | a month back / forward | | `H` / `L` | a month back / forward |
| `.` | today | | `.` | today |
| `Enter` | open the day's entry beside the calendar, or start one dated that day | | `Enter` | open the day's entry among the documents, or start one dated that day; `S` comes back |
| `q`, `Esc` | close the calendar's tile; `q` on an entry opened from it comes back here | | `q`, `Esc` | back to the documents |
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.
+3 -3
View File
@@ -71,9 +71,9 @@ The rail of buttons on the right does the same as the keys, for a mouse.
add a name and press `Enter`. `l` or `Enter` on the folder goes into it, and add a name and press `Enter`. `l` or `Enter` on the folder goes into it, and
`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 in a tile, and inbox of this space's board. Press `b` to see the board on the plan screen,
`Esc` to close it. `S` puts it up with the agenda and the calendar, and puts beside the agenda and the calendar, and `Esc` to go back to the documents.
them away again; `Alt-h`/`j`/`k`/`l` walk between the tiles. `S` goes to the plan and back; `Ctrl-h`/`j`/`k`/`l` walk between the tiles.
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.
+11 -10
View File
@@ -11,9 +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`. It opens in a tile when anything is late or due. Any other time, press `t`. It is on the plan
beside what is open; `S` puts it up with the board and the calendar, and puts screen, with the board and the calendar, in place of the documents; `S` goes
them away again, and `Alt-h`/`j`/`k`/`l` walk the tiles. there and back again, and `Ctrl-h`/`j`/`k`/`l` walk the tiles.
![The agenda](../images/agenda.png) ![The agenda](../images/agenda.png)
@@ -56,8 +56,9 @@ to it and press `L` instead: it opens in a **tile** of its own.
![Two documents in tiles](../images/tiles.png) ![Two documents in tiles](../images/tiles.png)
`Alt-h` and `Alt-l` move between the tiles and back to the list, `Alt-q` `h` and `l` move between the tiles and back to the list, `Ctrl-h` and
closes one, and `f` makes the focused one fill the screen. Each space remembers `Ctrl-l` from the editor too, `q` closes one, and `f` makes the focused one
fill the screen. Each space remembers
its tiles, so switching away and back finds them as they were. More in its tiles, so switching away and back finds them as they were. More in
[Spaces and documents](Spaces-and-Documents.md). [Spaces and documents](Spaces-and-Documents.md).
@@ -89,8 +90,8 @@ on the same line makes it a note on the scratchpad, and `Tab` again a timed line
in today's diary entry. A note waits there until `K`, `C` or `A` turns it into a in today's diary entry. A note waits there until `K`, `C` or `A` turns it into a
document, a card, or more of a document. document, a card, or more of a document.
Press `b` to see the **board**, in a tile beside the document. A digit there Press `b` to see the **board**, on the plan screen. A digit there shows the
shows the board of another space. board of another space.
![The board](../images/board.png) ![The board](../images/board.png)
@@ -99,9 +100,9 @@ hand. `h`/`l` move between lists and `j`/`k` between cards. `H`/`L` move a
card across, `Space` finishes it, and `D` sets its due date. `Enter` turns a card across, `Space` finishes it, and `D` sets its due date. `Enter` turns a
card over to its labels, description and checklist, in a tile beside, and card over to its labels, description and checklist, in a tile beside, and
`gf` opens its notes: a document made for the card the first time you ask, `gf` opens its notes: a document made for the card the first time you ask,
also beside, and `q` on it comes back to the board. `:repeat weekly` makes a opened among the documents, and `S` comes back to the board. `:repeat weekly` makes a
card come back in the inbox each time it is finished. `q` on the board closes card come back in the inbox each time it is finished. `q` on the board goes
its tile. back to the documents.
### Finding things ### Finding things
+4 -3
View File
@@ -103,11 +103,12 @@ Several documents can be open at once, each in a tile of its own.
it in the focused one. it in the focused one.
- Tiles share the space the way a tiling window manager does: two side by side, - Tiles share the space the way a tiling window manager does: two side by side,
a third splits the newer one, and so on. a third splits the newer one, and so on.
- `Alt-h/j/k/l` moves the focus between tiles, and `Alt-h` past the leftmost - `Ctrl-h/j/k/l` moves the focus between tiles, from the editor too, and
goes back to the list. `Alt-Shift-h/j/k/l` moves the tile itself. `Ctrl-h` past the leftmost goes back to the list. While reading, `h` and `l`
do the same. `Ctrl-Shift-h/j/k/l` moves the tile itself.
- `gt` / `gT` (or `:bn` / `:bp`) cycle through them, `:ls` lists them, and - `gt` / `gT` (or `:bn` / `:bp`) cycle through them, `:ls` lists them, and
`:b <n or name>` focuses one. `:b <n or name>` focuses one.
- `Alt-q` (or `:close`) closes the focused tile, `:only` every other one, and - `q` (or `:close`) closes the focused tile, `:only` every other one, and
`f` makes the focused tile fill the screen. `f` makes the focused tile fill the screen.
Opening a document that is already in a tile focuses that tile rather than Opening a document that is already in a tile focuses that tile rather than
+5 -1
View File
@@ -60,7 +60,10 @@ Each save keeps the text it replaces, so nothing written is lost. See
Not every note knows yet where it belongs. `Q` (or `:scratch`) lowers the Not every note knows yet where it belongs. `Q` (or `:scratch`) lowers the
scratchpad from under the bar, over whatever is on screen, the way the scratchpad from under the bar, over whatever is on screen, the way the
desktop lowers its own; `Q`, `q` or `Esc` raises it again, and what was on desktop lowers its own; `Q`, `q` or `Esc` raises it again, and what was on
screen is as it was. Its notes have no name, no space and no folder: a note is screen is as it was. `Ctrl-Q` works from anywhere, the editor included: the
scratchpad comes down with the last note being written, at its end, and
`Ctrl-Q` again puts it away and goes back to the document, still writing where
it was left. Firefox quits on `Ctrl-Q`, so there it is `Q` only. Its notes have no name, no space and no folder: a note is
a body and the time it was last touched, newest on top. a body and the time it was last touched, newest on top.
| key | what it does | | key | what it does |
@@ -73,6 +76,7 @@ a body and the time it was last touched, newest on top.
| `A` | put it at the end of a document (`:append [document]`) | | `A` | put it at the end of a document (`:append [document]`) |
| `dd` | drop the note (asks y/N) | | `dd` | drop the note (asks y/N) |
| `Q`, `q`, `Esc` | raise the scratchpad | | `Q`, `q`, `Esc` | raise the scratchpad |
| `Ctrl-Q` | from anywhere, while writing too: down with the last note being written, and back again |
A note is written in the same editor as a document, and saves itself the same A note is written in the same editor as a document, and saves itself the same
way. `Esc` in normal mode, or `:q`, goes back to the notes; a note left empty is way. `Esc` in normal mode, or `:q`, goes back to the notes; a note left empty is
+7 -2
View File
@@ -246,8 +246,8 @@
[[...keysOf('tree', 'down'), ...keysOf('tree', 'up')], 'browse, l opens, L opens beside'], [[...keysOf('tree', 'down'), ...keysOf('tree', 'up')], 'browse, l opens, L opens beside'],
[keysOf('global', 'space'), 'switch space, Tab cycles'], [keysOf('global', 'space'), 'switch space, Tab cycles'],
[keysOf('global', 'overview'), 'overview of every space'], [keysOf('global', 'overview'), 'overview of every space'],
[keysOf('global', 'plan'), 'board, agenda and calendar'], [keysOf('global', 'plan'), 'the plan: board, agenda, calendar'],
[[...keysOf('global', 'board'), ...keysOf('global', 'agenda'), ...keysOf('global', 'calendar')], 'one of them, in a tile'], [[...keysOf('global', 'board'), ...keysOf('global', 'agenda'), ...keysOf('global', 'calendar')], 'one of them, on the plan'],
[keysOf('tree', 'capture'), 'capture a card to the inbox'], [keysOf('tree', 'capture'), 'capture a card to the inbox'],
[keysOf('global', 'find'), 'find in every space'], [keysOf('global', 'find'), 'find in every space'],
[keysOf('tree', 'search'), 'search this space'], [keysOf('tree', 'search'), 'search this space'],
@@ -293,6 +293,7 @@
class="panes switch" class="panes switch"
class:reading={workspace.tiles.all.some((tile) => !tile.empty)} class:reading={workspace.tiles.all.some((tile) => !tile.empty)}
class:full={workspace.fullscreen} class:full={workspace.fullscreen}
class:plan={workspace.tiles.planning}
class:focus-doc={workspace.focus !== 'list'} class:focus-doc={workspace.focus !== 'list'}
style="--dir: {workspace.switchDirection}" style="--dir: {workspace.switchDirection}"
> >
@@ -433,6 +434,10 @@
height: calc(var(--h) * 100% - var(--gap)); height: calc(var(--h) * 100% - var(--gap));
} }
/* The plan screen: the views alone, with no list beside them. */
.panes.plan { grid-template-columns: 1fr; }
.panes.plan .window.list { display: none; }
/* `f`: the focused pane fills the porthole — the list, or one tile. */ /* `f`: the focused pane fills the porthole — the list, or one tile. */
.panes.full { grid-template-columns: 1fr; } .panes.full { grid-template-columns: 1fr; }
.panes.full.focus-doc .window.list, .panes.full.focus-doc .window.list,
+17 -7
View File
@@ -83,14 +83,18 @@
</div> </div>
<style> <style>
/* A tile's window is its frame, so it only fills it. */ /*
* A tile's window is its frame, so it only fills it — and the month is
* fitted into what the tile leaves it, never scrolled: the weeks share the
* height, up to a row a pointer still hits comfortably.
*/
.sheet { .sheet {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 12px;
height: 100%; height: 100%;
overflow-y: auto; overflow: hidden;
padding: 20px 24px; padding: 16px 20px;
} }
header { display: flex; align-items: center; gap: 12px; } header { display: flex; align-items: center; gap: 12px; }
@@ -99,21 +103,26 @@
header .title { color: var(--light); } header .title { color: var(--light); }
.grid { .grid {
flex: 0 1 auto;
min-height: 0;
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
gap: 4px; grid-template-rows: auto;
grid-auto-rows: minmax(0, 40px);
gap: 3px;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.weekday { text-align: center; font-size: 10px; color: var(--muted); } .weekday { text-align: center; font-size: 10px; color: var(--muted); }
.grid > li { min-height: 0; }
.day { .day {
position: relative; position: relative;
display: grid; display: grid;
place-items: center; place-items: center;
width: 100%; width: 100%;
aspect-ratio: 1.4; height: 100%;
padding: 0; padding: 0;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: var(--radius-s); border-radius: var(--radius-s);
@@ -126,12 +135,13 @@
.day.written { color: var(--fg); } .day.written { color: var(--fg); }
.day.today { border-color: var(--rim); } .day.today { border-color: var(--rim); }
.day.at { background: var(--light); color: var(--ink); } .day.at { background: var(--light); color: var(--ink); }
.dot, .many { position: absolute; bottom: 4px; } .dot, .many { position: absolute; bottom: 3px; }
.dot { width: 5px; height: 5px; border-radius: 999px; background: var(--light); } .dot { width: 5px; height: 5px; border-radius: 999px; background: var(--light); }
.many { font-size: 9px; color: var(--light); } .many { font-size: 9px; color: var(--light); }
.day.at :is(.dot) { background: var(--ink); } .day.at :is(.dot) { background: var(--ink); }
.day.at .many { color: var(--ink); } .day.at .many { color: var(--ink); }
.chosen { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.chosen h2 { .chosen h2 {
display: flex; display: flex;
align-items: center; align-items: center;
+9 -2
View File
@@ -155,6 +155,7 @@
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
import { api } from './api' import { api } from './api'
import { hooks } from './commands' import { hooks } from './commands'
import { claim } from './modes'
import { editorTheme, highlighting, languages } from './editorTheme' import { editorTheme, highlighting, languages } from './editorTheme'
import { embedSnippet } from './markdown' import { embedSnippet } from './markdown'
import { workspace, type Tile } from './store.svelte' import { workspace, type Tile } from './store.svelte'
@@ -180,9 +181,11 @@
/* /*
* Esc, before vim sees it: in normal mode, or with no vim, it leaves, as * Esc, before vim sees it: in normal mode, or with no vim, it leaves, as
* `:q` does — unless it is closing the completions. * `:q` does — unless it is closing the completions. The Ctrl keys heard
* everywhere — between the tiles, the scratchpad — go before vim too.
*/ */
function onkeydown(event: KeyboardEvent) { function onkeydown(event: KeyboardEvent) {
if (claim(event)) return
if (event.key !== 'Escape' || (workspace.vimEnabled && mode !== 'normal')) return if (event.key !== 'Escape' || (workspace.vimEnabled && mode !== 'normal')) return
if (view && completionStatus(view.state) !== null) return if (view && completionStatus(view.state) !== null) return
event.preventDefault() event.preventDefault()
@@ -317,6 +320,7 @@
parent: host!, parent: host!,
}) })
if (writing?.append) view.dispatch({ selection: { anchor: text.length } })
if (writing || workspace.tile === tile) view.focus() if (writing || workspace.tile === tile) view.focus()
const cm = workspace.vimEnabled ? getCM(view) : null const cm = workspace.vimEnabled ? getCM(view) : null
@@ -333,7 +337,10 @@
// A fresh entry starts in insert mode, and so does an empty short text, // A fresh entry starts in insert mode, and so does an empty short text,
// which is there to be written; anything else starts in normal mode, // which is there to be written; anything else starts in normal mode,
// the way opening a file in vim does. // the way opening a file in vim does.
if (tile?.enterInsert || (writing && !text.trim())) { if (writing?.append && text.trim()) {
Vim.handleKey(cm, 'G', 'workspace')
Vim.handleKey(cm, 'A', 'workspace')
} else if (tile?.enterInsert || (writing && !text.trim())) {
if (tile) tile.enterInsert = false if (tile) tile.enterInsert = false
Vim.handleKey(cm, 'i', 'workspace') Vim.handleKey(cm, 'i', 'workspace')
} }
+18 -13
View File
@@ -83,6 +83,7 @@
<footer class="label"> <footer class="label">
<span>writing — it saves itself</span> <span>writing — it saves itself</span>
<span><span class="kbd">esc</span> back to the notes</span> <span><span class="kbd">esc</span> back to the notes</span>
<span><span class="kbd">⌃ Q</span> back to where you were</span>
<span><span class="kbd">:keep</span> make it a document</span> <span><span class="kbd">:keep</span> make it a document</span>
</footer> </footer>
{:else} {:else}
@@ -92,31 +93,35 @@
<style> <style>
/* /*
* Hung from under the bar across the frame's opening, as the desktop's * The upper part of the porthole, come down over what is in it: the same
* scratchpad slides down from under its bar: hull, a rim, the lower corners * opening under the bar, the same rounded corners, and the focused
* rounded, and the light along its lower edge to say it has the keys. * window's lit rim, since it has the keys while it is down. It is
* uncovered from the top, as the desktop's scratchpad comes from under its
* bar.
*/ */
.drawer { .drawer {
--pane-bg: color-mix(in oklab, var(--surface) 40%, var(--hull));
position: absolute; position: absolute;
top: calc(var(--bar-h) + var(--frame)); top: var(--bar-h);
left: calc(var(--frame) + 24px); left: var(--frame);
right: calc(var(--rail-w) + 24px); right: var(--rail-w);
z-index: 25; z-index: 25;
height: min(62vh, 640px); height: min(62vh, 640px, calc(100% - var(--bar-h) - var(--status-h)));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
padding: 14px 20px 12px; padding: 14px 20px 12px;
background: var(--hull); border: 1px solid transparent;
border: 1px solid var(--surface-alt); border-radius: var(--frame-radius);
border-top: 0; background:
border-bottom-color: var(--light-dim); linear-gradient(var(--pane-bg), var(--pane-bg)) padding-box,
border-radius: 0 0 var(--frame-radius) var(--frame-radius); linear-gradient(135deg, var(--light), var(--light-dim)) border-box;
box-shadow: 0 18px 40px color-mix(in oklab, var(--ink) 70%, transparent); box-shadow: 0 18px 40px color-mix(in oklab, var(--ink) 70%, transparent);
animation: lower var(--t-slow) var(--ease-snap) both; animation: lower var(--t-slow) var(--ease-snap) both;
} }
@keyframes lower { @keyframes lower {
from { transform: translateY(-104%); } from { clip-path: inset(0 0 100% 0 round var(--frame-radius)); }
to { clip-path: inset(0 0 0 0 round var(--frame-radius)); }
} }
header { display: flex; align-items: center; gap: 10px; } header { display: flex; align-items: center; gap: 10px; }
+1 -1
View File
@@ -135,7 +135,7 @@
actions(index) actions(index)
}} }}
class:active={index === workspace.tree.cursor} class:active={index === workspace.tree.cursor}
class:open={workspace.tiles.all.some((tile) => tile.open?.id === node.id)} class:open={workspace.tiles.every.some((tile) => tile.open?.id === node.id)}
class:marked={workspace.tree.isMarked(node.id)} class:marked={workspace.tree.isMarked(node.id)}
class:nested class:nested
data-index={index} data-index={index}
+7 -1
View File
@@ -7,7 +7,13 @@
const { pending = '' }: { pending?: string } = $props() const { pending = '' }: { pending?: string } = $props()
const mode = $derived( const mode = $derived(
workspace.editing ? (workspace.vimEnabled ? workspace.vimMode.toUpperCase() : 'EDIT') : 'BROWSE', workspace.editing
? workspace.vimEnabled
? workspace.vimMode.toUpperCase()
: 'EDIT'
: workspace.tiles.planning
? 'PLAN'
: 'BROWSE',
) )
// The path is the file name here: work/einarbeiten/01-was-ist-eebus. // The path is the file name here: work/einarbeiten/01-was-ist-eebus.
+25 -13
View File
@@ -82,7 +82,7 @@ function focusPane(pane: 'list' | 'document') {
} }
/* /*
* Alt + h/j/k/l: the focus goes to the tile that way, out of the editor * Ctrl + h/j/k/l: the focus goes to the tile that way, out of the editor
* 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.
*/ */
@@ -93,7 +93,8 @@ function tileKey(direction: Direction, shift: boolean) {
} }
const next = neighbour(dwindle(workspace.tiles.all.length), workspace.tiles.focused, direction) const next = neighbour(dwindle(workspace.tiles.all.length), workspace.tiles.focused, direction)
if (next < 0) { if (next < 0) {
if (direction === 'h' && !shift) focusPane('list') // The plan screen has no list beside it.
if (direction === 'h' && !shift && !workspace.tiles.planning) focusPane('list')
return return
} }
if (!shift) return void focusTile(next) if (!shift) return void focusTile(next)
@@ -103,15 +104,24 @@ function tileKey(direction: Direction, shift: boolean) {
workspace.tiles.focused = next workspace.tiles.focused = next
} }
/** The drawer up, and the keys back where they were: in the editor, when a document was being written. */
async function raise() {
await workspace.scratch.close()
if (!workspace.scratch.open && workspace.editing && workspace.pane === 'document') hooks.focusEditor()
}
export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = { export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
always: { always: {
save: () => { save: () => {
if (workspace.drafts.writing) guard(() => workspace.drafts.saveWriting()) if (workspace.drafts.writing) guard(() => workspace.drafts.saveWriting())
else if (workspace.open) guard(() => workspace.drafts.save()) else if (workspace.open) guard(() => workspace.drafts.save())
}, },
close: () => void runCommand('close'), // Under the drawer the tiles are out of sight, so the keys are the note's.
focus: (key) => tileKey(key.slice(-1) as Direction, false), focus: (key) => (workspace.scratch.open ? false : tileKey(key.slice(-1) as Direction, false)),
swap: (key) => tileKey(key.slice(-1) as Direction, true), swap: (key) => (workspace.scratch.open ? false : tileKey(key.slice(-1).toLowerCase() as Direction, true)),
// Ctrl-Q: down with the last note being written, and up again back to
// what was being done — the document still in the mode it was left in.
scratch: () => guard(() => (workspace.scratch.open ? raise() : workspace.scratch.jot())),
}, },
sheet: { sheet: {
@@ -318,7 +328,7 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
}, },
line: () => line(':'), line: () => line(':'),
help: () => void runCommand('help'), help: () => void runCommand('help'),
raise: () => void workspace.scratch.close(), raise: () => guard(raise),
nothing: () => {}, nothing: () => {},
}, },
@@ -355,6 +365,8 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
open: (key) => { open: (key) => {
const enter = key === 'Enter' || key === 'Shift-Enter' const enter = key === 'Enter' || key === 'Shift-Enter'
if (enter && reading() && hooks.fileAction()) return if (enter && reading() && hooks.fileAction()) return
// Reading, `l` is the tile to the right, as `h` is the one to the left.
if (!enter && workspace.pane === 'document' && workspace.open) return tileKey('l', false)
// Shift-Enter, like `L`: beside what is open, in a tile of its own. // Shift-Enter, like `L`: beside what is open, in a tile of its own.
if (key === 'Shift-Enter' && !reading()) return guard(() => workspace.tiles.openBeside()) if (key === 'Shift-Enter' && !reading()) return guard(() => workspace.tiles.openBeside())
const shown = workspace.tiles.all.findIndex((tile) => tile.open && tile.open.id === workspace.tree.selected?.id) const shown = workspace.tiles.all.findIndex((tile) => tile.open && tile.open.id === workspace.tree.selected?.id)
@@ -364,10 +376,10 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
beside: () => { beside: () => {
if (!reading()) guard(() => workspace.tiles.openBeside()) if (!reading()) guard(() => workspace.tiles.openBeside())
}, },
// Leftwards at every depth: from the document to the list, from the // Leftwards at every depth: from a document to the tile left of it, from
// list up a level. // the leftmost to the list, from the list up a level.
left: () => { left: () => {
if (workspace.open && workspace.pane === 'document') focusPane('list') if (workspace.open && workspace.pane === 'document') tileKey('h', false)
else guard(() => workspace.tree.up()) else guard(() => workspace.tree.up())
}, },
sideways: (key) => { sideways: (key) => {
@@ -516,7 +528,7 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
open: () => void workspace.guard(() => workspace.diary.openCalendarDay()).then(() => hooks.focusEditor()), open: () => void workspace.guard(() => workspace.diary.openCalendarDay()).then(() => hooks.focusEditor()),
}, },
/** What every view shares: `q` closes its tile, back to the one before. */ /** What every view shares: `q` closes a card's back, and from the others goes back to the documents. */
views: { views: {
close: () => void runCommand('close'), close: () => void runCommand('close'),
}, },
@@ -549,9 +561,9 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
agenda: () => void runCommand('agenda'), agenda: () => void runCommand('agenda'),
today: () => void runCommand('today'), today: () => void runCommand('today'),
calendar: () => void runCommand('cal'), calendar: () => void runCommand('cal'),
// `S`, as the desktop's special workspace is bound: the board, the // `S`, as the desktop's special workspace is bound: the plan screen —
// agenda and the calendar, put up and away again. `b`, `t` and `C` only // the board, the agenda and the calendar — and back to the documents.
// ever go to their tile. // `b`, `t` and `C` go there too, to their own view.
plan: () => void runCommand('plan'), plan: () => void runCommand('plan'),
line: () => line(':'), line: () => line(':'),
help: () => void runCommand('help'), help: () => void runCommand('help'),
+3 -3
View File
@@ -591,14 +591,14 @@ export const commands: CommandSpec[] = [
{ {
name: 'board', name: 'board',
group: 'board', group: 'board',
help: "this space's board, in a tile — the last one shown when this space has none", help: "this space's board, on the plan screen — the last one shown when this space has none",
run: () => workspace.guard(() => workspace.tiles.show('board')), run: () => workspace.guard(() => workspace.tiles.show('board')),
}, },
{ {
name: 'plan', name: 'plan',
aliases: ['special'], aliases: ['special'],
group: 'board', group: 'board',
help: 'the board, the agenda and the calendar, tiled ahead of what is open — S puts them up, and every view away again; 1 … 9 on the board picks the board', help: 'the plan screen: the board, the agenda and the calendar, in place of the documents — S goes there and back again; 1 … 9 on the board picks the board',
run: () => workspace.guard(() => workspace.tiles.togglePlan()), run: () => workspace.guard(() => workspace.tiles.togglePlan()),
}, },
{ {
@@ -874,7 +874,7 @@ export const commands: CommandSpec[] = [
name: 'close', name: 'close',
aliases: ['clo'], aliases: ['clo'],
group: 'tiles', group: 'tiles',
help: 'close the focused tile — Alt-q; asks when its draft could not be written', help: 'close the focused tile — q while reading; on the plan screen, back to the documents; asks when its draft could not be written',
run: () => run: () =>
workspace.guard(async () => { workspace.guard(async () => {
await workspace.drafts.flushTile() await workspace.drafts.flushTile()
+11 -11
View File
@@ -46,7 +46,7 @@ export interface Binding {
context: Context context: Context
/** /**
* What is pressed, as `keyName` spells it: `j`, `G`, `Enter`, `Space`, * What is pressed, as `keyName` spells it: `j`, `G`, `Enter`, `Space`,
* `Shift-Tab`, `Ctrl-d`, `Alt-h`, `1`, `Shift-1`, a two-key `gg`, or `*` * `Shift-Tab`, `Ctrl-d`, `Ctrl-H`, `1`, `Shift-1`, a two-key `gg`, or `*`
* for any key without Ctrl, Alt or Meta. A binding without keys only * for any key without Ctrl, Alt or Meta. A binding without keys only
* describes — a mouse gesture, the editor's own keymap. * describes — a mouse gesture, the editor's own keymap.
*/ */
@@ -78,12 +78,11 @@ export const bindings = [
{ context: 'global', keys: ['Tab', 'Shift-Tab'], action: 'cycle', label: 'Tab · Shift-Tab', help: 'next / previous space' }, { context: 'global', keys: ['Tab', 'Shift-Tab'], action: 'cycle', label: 'Tab · Shift-Tab', help: 'next / previous space' },
{ context: 'global', keys: ['w'], action: 'overview', help: 'overview of every space — hjkl, then Enter' }, { context: 'global', keys: ['w'], action: 'overview', help: 'overview of every space — hjkl, then Enter' },
{ context: 'global', keys: ['f'], action: 'fullscreen', help: 'fullscreen the focused pane or tile; again or Esc to leave' }, { context: 'global', keys: ['f'], action: 'fullscreen', help: 'fullscreen the focused pane or tile; again or Esc to leave' },
{ context: 'always', keys: ['Alt-h', 'Alt-j', 'Alt-k', 'Alt-l'], action: 'focus', typing: true, label: 'Alt-h/j/k/l', help: 'focus the tile that way, from the editor too — left of the leftmost is the list', essential: true }, { context: 'always', keys: ['Ctrl-h', 'Ctrl-j', 'Ctrl-k', 'Ctrl-l'], action: 'focus', typing: true, label: 'Ctrl-h/j/k/l', help: 'focus the tile that way, from the editor too — left of the leftmost is the list; h / l do the same while reading', essential: true },
{ context: 'always', keys: ['Alt-Shift-h', 'Alt-Shift-j', 'Alt-Shift-k', 'Alt-Shift-l'], action: 'swap', typing: true, label: 'Alt-Shift-h/j/k/l', help: 'move the focused tile that way' }, { context: 'always', keys: ['Ctrl-H', 'Ctrl-J', 'Ctrl-K', 'Ctrl-L'], action: 'swap', typing: true, label: 'Ctrl-Shift-h/j/k/l', help: 'move the focused tile that way' },
{ context: 'tree', keys: ['L'], action: 'beside', label: 'L · Shift-Enter', help: 'open the row in a new tile beside the others' }, { context: 'tree', keys: ['L'], action: 'beside', label: 'L · Shift-Enter', help: 'open the row in a new tile beside the others' },
{ context: 'global', keys: ['gt'], action: 'nextTile', label: 'gt / gT', help: 'next / previous tile — a view too' }, { context: 'global', keys: ['gt'], action: 'nextTile', label: 'gt / gT', help: 'next / previous tile — a view too' },
{ context: 'global', keys: ['gT'], action: 'previousTile' }, { context: 'global', keys: ['gT'], action: 'previousTile' },
{ context: 'always', keys: ['Alt-q'], action: 'close', typing: true, help: 'close the focused tile — :close; :only closes the others' },
{ context: 'tree', keys: ['j', 'ArrowDown'], action: 'down', label: 'j / k · ↓ ↑', help: 'move down / up the list — scroll the document when it has focus', essential: true }, { context: 'tree', keys: ['j', 'ArrowDown'], action: 'down', label: 'j / k · ↓ ↑', help: 'move down / up the list — scroll the document when it has focus', essential: true },
{ context: 'tree', keys: ['k', 'ArrowUp'], action: 'up' }, { context: 'tree', keys: ['k', 'ArrowUp'], action: 'up' },
{ context: 'tree', keys: ['gg'], action: 'top', label: 'gg / G', help: 'first / last row, or top / bottom of the document — a first g, d, ] or [ waits a second in the status line' }, { context: 'tree', keys: ['gg'], action: 'top', label: 'gg / G', help: 'first / last row, or top / bottom of the document — a first g, d, ] or [ waits a second in the status line' },
@@ -109,14 +108,14 @@ export const bindings = [
{ context: 'tree', keys: ['za'], action: 'files', help: "a document's own files, folded open under it in the list — again folds them away; p drops marked files in there" }, { context: 'tree', keys: ['za'], action: 'files', help: "a document's own files, folded open under it in the list — again folds them away; p drops marked files in there" },
{ context: 'tree', keys: ['M'], action: 'markAll', help: 'pick up the whole folder — again puts it all back' }, { context: 'tree', keys: ['M'], action: 'markAll', help: 'pick up the whole folder — again puts it all back' },
{ context: 'tree', label: 'Ctrl / Shift-click', help: 'pick a row up · every row up to it — drag them onto a folder' }, { context: 'tree', label: 'Ctrl / Shift-click', help: 'pick a row up · every row up to it — drag them onto a folder' },
{ context: 'global', keys: ['S'], action: 'plan', help: 'the board, the agenda and the calendar, tiled ahead of what is open with the board focused — S again puts every view away', essential: true }, { context: 'global', keys: ['S'], action: 'plan', help: 'the plan screen: the board, the agenda and the calendar, in place of the documents and the list — S again goes back to the documents as they were', essential: true },
{ context: 'global', keys: ['t'], action: 'agenda', help: 'agenda — late, due this week, in hand, on every board — in a tile of its own', essential: true }, { context: 'global', keys: ['t'], action: 'agenda', help: 'agenda — late, due this week, in hand, on every board — on the plan screen', essential: true },
{ context: 'global', keys: ['T'], action: 'today', help: "today's diary entry, started if there is none — from templates/day when the diary has one", essential: true }, { context: 'global', keys: ['T'], action: 'today', help: "today's diary entry, started if there is none — from templates/day when the diary has one", essential: true },
{ context: 'global', keys: ['C'], action: 'calendar', help: 'the diary by month, in a tile: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day beside it' }, { context: 'global', keys: ['C'], action: 'calendar', help: 'the diary by month, on the plan screen: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day among the documents' },
{ context: 'tree', keys: ['[d'], action: 'previousEntry', label: '[d / ]d', help: 'the diary entry before / after the one open, by date' }, { context: 'tree', keys: ['[d'], action: 'previousEntry', label: '[d / ]d', help: 'the diary entry before / after the one open, by date' },
{ context: 'tree', keys: [']d'], action: 'nextEntry' }, { context: 'tree', keys: [']d'], action: 'nextEntry' },
{ context: 'global', keys: ['Ctrl-p'], action: 'find', label: 'Ctrl-P', help: 'find anything, in every space — Enter opens the result lit beside them, n goes on', essential: true }, { context: 'global', keys: ['Ctrl-p'], action: 'find', label: 'Ctrl-P', help: 'find anything, in every space — Enter opens the result lit beside them, n goes on', essential: true },
{ context: 'global', keys: ['b'], action: 'board', help: "the board, in a tile: this space's, or the last one shown — 1 … 9 there shows another's" }, { context: 'global', keys: ['b'], action: 'board', help: "the board, on the plan screen: this space's, or the last one shown — 1 … 9 there shows another's" },
{ context: 'tree', keys: ['c'], action: 'capture', help: "capture a card into this space's inbox, without leaving here — reading a document, a card about it; Tab on the line makes it a note, or a line in today's entry", essential: true }, { context: 'tree', keys: ['c'], action: 'capture', help: "capture a card into this space's inbox, without leaving here — reading a document, a card about it; Tab on the line makes it a note, or a line in today's entry", essential: true },
{ context: 'tree', keys: ['q'], action: 'close', label: 'Esc · q', help: 'close the document, and its tile when there are several — Esc first puts down what m picked up', essential: true }, { context: 'tree', keys: ['q'], action: 'close', label: 'Esc · q', help: 'close the document, and its tile when there are several — Esc first puts down what m picked up', essential: true },
{ context: 'tree', keys: ['Escape'], action: 'escape' }, { context: 'tree', keys: ['Escape'], action: 'escape' },
@@ -136,12 +135,13 @@ export const bindings = [
// --- the workspace around the panes ------------------------------------------ // --- the workspace around the panes ------------------------------------------
{ context: 'workspace', keys: ['Escape'], action: 'escape', typing: true }, { context: 'workspace', keys: ['Escape'], action: 'escape', typing: true },
{ context: 'workspace', keys: ['Q'], action: 'scratch', section: 'scratch', help: 'lower the scratchpad over whatever is on screen, or raise it again — :scratch <text> takes a note without it', essential: true }, { context: 'workspace', keys: ['Q'], action: 'scratch', section: 'scratch', help: 'lower the scratchpad over whatever is on screen, or raise it again — :scratch <text> takes a note without it', essential: true },
{ context: 'always', keys: ['Ctrl-q'], action: 'scratch', typing: true, section: 'scratch', label: 'Ctrl-Q', help: 'from anywhere, the editor too: the scratchpad down with the last note being written, and again back to where you were, still writing — Firefox quits on it instead', essential: true },
// While a document is being written, a key that lands outside the editor // While a document is being written, a key that lands outside the editor
// hands the focus back to it instead of walking the list unseen. // hands the focus back to it instead of walking the list unseen.
{ context: 'writing', keys: ['*'], action: 'refocus' }, { context: 'writing', keys: ['*'], action: 'refocus' },
// --- what the views share: the board, the agenda, the calendar, a card's back // --- what the views share: the board, the agenda, the calendar, a card's back
{ context: 'views', keys: ['q', 'Escape'], action: 'close', label: 'q · Esc', caps: ['q'], hint: 'close', help: "in a view — the board, the agenda, the calendar, a card's back — close its tile; the focus goes back to the tile before" }, { context: 'views', keys: ['q', 'Escape'], action: 'close', label: 'q · Esc', caps: ['q'], hint: 'close', help: "on the plan screen: from a card's back to the board, from the board, the agenda or the calendar back to the documents" },
// --- on a board --------------------------------------------------------------- // --- on a board ---------------------------------------------------------------
{ context: 'board', keys: ['h', 'ArrowLeft'], action: 'left', label: 'h / l', caps: ['h', 'j', 'k', 'l'], hint: 'move', help: 'to the list left / right — right of the last, to the tile beside' }, { context: 'board', keys: ['h', 'ArrowLeft'], action: 'left', label: 'h / l', caps: ['h', 'j', 'k', 'l'], hint: 'move', help: 'to the list left / right — right of the last, to the tile beside' },
@@ -156,7 +156,7 @@ export const bindings = [
{ context: 'board', keys: ['L'], action: 'sendRight' }, { context: 'board', keys: ['L'], action: 'sendRight' },
{ context: 'board', keys: ['Space'], action: 'done', hint: 'done', help: 'into the done list, or back out to the inbox' }, { context: 'board', keys: ['Space'], action: 'done', hint: 'done', help: 'into the done list, or back out to the inbox' },
{ context: 'board', keys: ['Enter'], action: 'turn', hint: 'turn over', help: 'turn the card over, in a tile beside: its labels, description and checklist' }, { context: 'board', keys: ['Enter'], action: 'turn', hint: 'turn over', help: 'turn the card over, in a tile beside: its labels, description and checklist' },
{ context: 'board', keys: ['gf'], action: 'notes', help: "the card's notes — a document, made on first use; q comes back" }, { context: 'board', keys: ['gf'], action: 'notes', help: "the card's notes — a document, made on first use, opened among the documents; S comes back" },
{ context: 'board', keys: ['o', 'c'], action: 'new', label: 'o · c', caps: ['o'], hint: 'new', help: 'new card on this list' }, { context: 'board', keys: ['o', 'c'], action: 'new', label: 'o · c', caps: ['o'], hint: 'new', help: 'new card on this list' },
{ context: 'board', keys: ['D'], action: 'due', hint: 'due', help: 'due date: fri, tm, +3d, 10-31 …' }, { context: 'board', keys: ['D'], action: 'due', hint: 'due', help: 'due date: fri, tm, +3d, 10-31 …' },
{ context: 'board', keys: ['R'], action: 'rename', hint: 'rename', help: 'rename the card, or the list when it is empty' }, { context: 'board', keys: ['R'], action: 'rename', hint: 'rename', help: 'rename the card, or the list when it is empty' },
@@ -183,7 +183,7 @@ export const bindings = [
{ context: 'card', keys: ['D'], action: 'due', hint: 'due' }, { context: 'card', keys: ['D'], action: 'due', hint: 'due' },
{ context: 'card', keys: ['H'], action: 'sendLeft', label: 'H / L', caps: ['H', 'L'], hint: 'send', help: 'send the card to the list left / right' }, { context: 'card', keys: ['H'], action: 'sendLeft', label: 'H / L', caps: ['H', 'L'], hint: 'send', help: 'send the card to the list left / right' },
{ context: 'card', keys: ['L'], action: 'sendRight' }, { context: 'card', keys: ['L'], action: 'sendRight' },
{ context: 'card', keys: ['gf'], action: 'notes', hint: 'notes', help: "the card's notes — a document, made on first use; q comes back" }, { context: 'card', keys: ['gf'], action: 'notes', hint: 'notes', help: "the card's notes — a document, made on first use, opened among the documents; S comes back" },
{ context: 'card', keys: [':'], action: 'line' }, { context: 'card', keys: [':'], action: 'line' },
// --- the scratchpad, while it is down -------------------------------------------- // --- the scratchpad, while it is down --------------------------------------------
+18 -2
View File
@@ -3,7 +3,7 @@
* contexts in keymap.ts, the topmost first. A key goes down the stack until * contexts in keymap.ts, the topmost first. A key goes down the stack until
* a context takes it. * a context takes it.
* *
* always Ctrl-S, Alt-h/j/k/l — heard in the editor too * always Ctrl-S, Ctrl-Q, Ctrl-h/j/k/l — heard in the editor too
* a sheet help, info, trash …; nothing under one hears a key * a sheet help, info, trash …; nothing under one hears a key
* drawer the scratchpad, while it is down * drawer the scratchpad, while it is down
* workspace Esc out of fullscreen, Q * workspace Esc out of fullscreen, Q
@@ -52,7 +52,7 @@ const lone = new Set(['Shift', 'Control', 'Alt', 'AltGraph', 'Meta', 'CapsLock',
/** /**
* A key as the table spells it: `j`, `G`, `Enter`, `Shift-Tab`, `Ctrl-d`, * A key as the table spells it: `j`, `G`, `Enter`, `Shift-Tab`, `Ctrl-d`,
* `Alt-Shift-h`, `1`, `Shift-1`. Digits, and letters under Alt, are read * `Ctrl-H`, `1`, `Shift-1`. Digits, and letters under Alt, are read
* from `event.code`, so they are the same key on every layout — and on a * from `event.code`, so they are the same key on every layout — and on a
* Mac, where Alt turns h into ˙. Shift is only spelled out where the key * Mac, where Alt turns h into ˙. Shift is only spelled out where the key
* itself does not already say it. A modifier on its own is no key at all. * itself does not already say it. A modifier on its own is no key at all.
@@ -134,3 +134,19 @@ export function dispatch(event: KeyboardEvent, pending: string, line: boolean, t
} }
return '' return ''
} }
/**
* A Ctrl key heard in every context, taken before the editor sees it: vim
* has uses of its own for Ctrl-h and Ctrl-k, and what it handles never
* reaches `dispatch`. Ctrl-S is left to the editor, which saves its own
* text. True when it was taken.
*/
export function claim(event: KeyboardEvent): boolean {
const name = keyName(event)
if (!name?.startsWith('Ctrl-')) return false
const binding = find('always', name)
if (!binding?.typing || binding.action === 'save' || !run(binding, name, event)) return false
event.preventDefault()
event.stopPropagation()
return true
}
+1 -1
View File
@@ -334,7 +334,7 @@ export class Cards {
/** The cards about each document open in a tile, read again. */ /** The cards about each document open in a tile, read again. */
async #reloadCardsAbout() { async #reloadCardsAbout() {
await Promise.all( await Promise.all(
this.ws.tiles.all.map(async (tile) => { this.ws.tiles.every.map(async (tile) => {
const id = tile.open?.kind === 'document' ? tile.open.id : null const id = tile.open?.kind === 'document' ? tile.open.id : null
if (id === null) return if (id === null) return
const cards = await api.nodeCards(id).catch(() => null) const cards = await api.nodeCards(id).catch(() => null)
+4 -4
View File
@@ -89,7 +89,7 @@ export class Drafts {
/** Back online: what is waiting goes now rather than at the next retry. */ /** Back online: what is waiting goes now rather than at the next retry. */
watchConnection() { watchConnection() {
window.addEventListener('online', () => { window.addEventListener('online', () => {
for (const tile of this.ws.tiles.all) { for (const tile of this.ws.tiles.every) {
if (!tile.dirty) continue if (!tile.dirty) continue
tile.stopAutosave() tile.stopAutosave()
void this.#writeBack(tile) void this.#writeBack(tile)
@@ -107,7 +107,7 @@ export class Drafts {
* unsaved work without a word. * unsaved work without a word.
*/ */
async flush() { async flush() {
for (const tile of this.ws.tiles.all) await this.flushTile(tile) for (const tile of this.ws.tiles.every) await this.flushTile(tile)
} }
/** The same for one tile only, as it is about to show something else. */ /** The same for one tile only, as it is about to show something else. */
@@ -173,7 +173,7 @@ export class Drafts {
* then refused, and `:e!` takes the rewritten text. * then refused, and `:e!` takes the rewritten text.
*/ */
async reread() { async reread() {
for (const tile of this.ws.tiles.all) { for (const tile of this.ws.tiles.every) {
if (!tile.open || tile.editing || tile.dirty) continue if (!tile.open || tile.editing || tile.dirty) continue
const node = await api.getNode(tile.open.id).catch(() => null) const node = await api.getNode(tile.open.id).catch(() => null)
if (!node) continue if (!node) continue
@@ -236,7 +236,7 @@ export class Drafts {
/** Every tile showing `id` reads it again, after it was written somewhere else. */ /** Every tile showing `id` reads it again, after it was written somewhere else. */
async rereadNode(id: number) { async rereadNode(id: number) {
for (const tile of this.ws.tiles.all) { for (const tile of this.ws.tiles.every) {
if (tile.open?.id !== id || tile.dirty) continue if (tile.open?.id !== id || tile.dirty) continue
const node = await api.getNode(id) const node = await api.getNode(id)
tile.open = node tile.open = node
+12
View File
@@ -43,6 +43,18 @@ export class Scratchpad {
this.open = false this.open = false
} }
/**
* Ctrl-Q, from anywhere: the drawer down with the note written last being
* written on, at its end — or, when it is down, up again.
*/
async jot() {
if (this.open) return this.close()
await this.toggle()
this.cursor = 0
this.editNote()
if (this.ws.drafts.writing) this.ws.drafts.writing.append = true
}
/** `o`: a new note at the top, written straight away. */ /** `o`: a new note at the top, written straight away. */
newNote() { newNote() {
this.#writeNote(null, '') this.#writeNote(null, '')
+70 -40
View File
@@ -1,6 +1,6 @@
import { isContainer, type WorkspaceNode } from '../api' import { isContainer, type WorkspaceNode } from '../api'
import { formatDay } from '../util' import { formatDay } from '../util'
import type { Workspace } from '../store.svelte' import type { Pane, Workspace } from '../store.svelte'
/** /**
* What a tile shows: a node of the tree — a document or a file — or one of * What a tile shows: a node of the tree — a document or a file — or one of
@@ -68,9 +68,10 @@ export class Tile {
} }
/** /**
* The tiles on screen and which has the focus: opened, closed, walked, and * The tiles on screen and which has the focus: opened, closed, walked. There
* the views — the board, the agenda, the calendar, a card's back — put up * are two screens of them, and one is shown at a time: the documents, beside
* among them. * the list, and the plan — the board, the agenda, the calendar, a card's
* back — which `S` goes to and comes back from.
*/ */
export class Tiles { export class Tiles {
readonly ws: Workspace readonly ws: Workspace
@@ -91,9 +92,19 @@ export class Tiles {
return this.all[this.focused] ?? this.all[0] return this.all[this.focused] ?? this.all[0]
} }
/** Every tile showing `id`. */ /** The plan screen is on show, and the documents are put away behind it. */
planning = $state(false)
/** The screen not on show, as it was left. */
#away = $state<{ all: Tile[]; focused: number; trail: Tile[]; pane: Pane }>({ all: [], focused: 0, trail: [], pane: 'list' })
/** The tiles of both screens: a draft behind the plan is still written. */
get every(): Tile[] {
return [...this.all, ...this.#away.all]
}
/** Every tile showing `id`, on either screen. */
showing(id: number): Tile[] { showing(id: number): Tile[] {
return this.all.filter((tile) => tile.open?.id === id) return this.every.filter((tile) => tile.open?.id === id)
} }
/* /*
@@ -158,8 +169,13 @@ export class Tiles {
if (this.current.empty) this.ws.pane = 'list' if (this.current.empty) this.ws.pane = 'list'
} }
/** `:close`: the focused tile goes, or with only one, what it shows. */ /**
* `:close`: the focused tile goes, or with only one, what it shows. On the
* plan only a card's back goes; closing the board, the agenda or the
* calendar goes back to the documents.
*/
close() { close() {
if (this.planning && this.current.kind !== 'card') return this.leavePlan()
this.ws.drafts.leaveCard() this.ws.drafts.leaveCard()
this.current.clear() this.current.clear()
this.prune() this.prune()
@@ -201,46 +217,60 @@ export class Tiles {
/* /*
* The views — the board, the agenda, the calendar, a card's back — are * The views — the board, the agenda, the calendar, a card's back — are
* tiles, opened beside what is on screen. Each is read again whenever it is * the tiles of the plan screen. Each is read again whenever it is gone to:
* gone to: it is looked at in passing, and what it says should be what is * it is looked at in passing, and what it says should be what is true now.
* true now.
*/ */
/** `b`, `t`, `C`, Enter on a card: to that view's tile, opening it first when it is not up. */ /** The views the plan screen holds, in the order they are tiled. */
static readonly PLAN: View[] = ['board', 'agenda', 'calendar']
/** The other screen on show, and this one put away as it is. */
swap() {
const away = this.#away
this.#away = { all: this.all, focused: this.focused, trail: this.trail, pane: this.ws.pane }
this.all = away.all.length > 0 ? away.all : [new Tile()]
this.focused = Math.min(away.focused, this.all.length - 1)
this.trail = away.trail
this.ws.pane = away.pane
this.planning = !this.planning
}
/** To the plan screen, with every view on it, from wherever the documents were. */
enterPlan() {
if (this.planning) return
this.ws.drafts.leaveCard()
this.swap()
const cards = this.all.filter((tile) => tile.kind === 'card')
this.all = [...Tiles.PLAN.map((kind) => this.view(kind) ?? new Tile(kind)), ...cards]
this.focused = Math.min(this.focused, this.all.length - 1)
this.ws.pane = 'document'
}
/** Back to the documents, as they were left. */
leavePlan() {
if (!this.planning) return
this.ws.drafts.leaveCard()
this.swap()
// What was closed behind the plan — deleted, moved away — is gone from here too.
this.prune()
if (!this.current.empty) this.focusOn(this.focused)
}
/** `b`, `t`, `C`, Enter on a card: to the plan screen, and that view on it focused. */
async show(kind: View) { async show(kind: View) {
await this.loadView(kind) // Coming to the plan, every view on it is read again, not only this one.
if (!this.view(kind)) { const kinds = this.planning ? [kind] : Tiles.PLAN
// The one empty tile there can be takes it; otherwise it goes beside. await Promise.all(kinds.map((each) => this.loadView(each)))
if (this.current.empty) this.current.kind = kind this.enterPlan()
else this.all.push(new Tile(kind)) if (!this.view(kind)) this.all.push(new Tile(kind))
}
this.focusOn(this.all.indexOf(this.view(kind)!)) this.focusOn(this.all.indexOf(this.view(kind)!))
if (kind === 'board' && this.ws.cards.boardSpaceId === null) this.ws.say('no space has a board — :set board turns one on', 'error') if (kind === 'board' && this.ws.cards.boardSpaceId === null) this.ws.say('no space has a board — :set board turns one on', 'error')
} }
/** The views `S` puts up together, in the order they are tiled. */ /** `S`: the plan screen with the board focused, or back to the documents. */
static readonly PLAN: View[] = ['board', 'agenda', 'calendar']
/**
* `S`: the board, the agenda and the calendar, tiled ahead of the
* documents with the board focused — or, when all three are up, every view
* put away again.
*/
async togglePlan() { async togglePlan() {
if (Tiles.PLAN.every((kind) => this.view(kind))) { if (this.planning) return this.leavePlan()
for (const tile of this.all) if (tile.kind !== 'node') tile.clear() await this.show('board')
this.prune()
if (!this.current.empty) this.focusOn(this.focused)
return
}
await Promise.all(Tiles.PLAN.map((kind) => this.loadView(kind)))
const current = this.current
const missing = Tiles.PLAN.filter((kind) => !this.view(kind)).map((kind) => new Tile(kind))
this.all = [...missing, ...this.all]
this.focused = this.all.indexOf(current)
this.prune()
this.focusOn(this.all.indexOf(this.view('board')!))
if (this.ws.cards.boardSpaceId === null) this.ws.say('no space has a board — :set board turns one on', 'error')
} }
/** Read again what a view shows. */ /** Read again what a view shows. */
@@ -253,7 +283,7 @@ export class Tiles {
} }
} }
/** Every view on screen read again, after a space switch brought them. */ /** Every view on the plan read again, after a space switch. */
async loadViews() { async loadViews() {
if (this.view('board') || this.view('card')) await this.ws.cards.loadBoard() if (this.view('board') || this.view('card')) await this.ws.cards.loadBoard()
if (this.view('calendar')) await this.loadView('calendar') if (this.view('calendar')) await this.loadView('calendar')
+42 -42
View File
@@ -5,9 +5,10 @@ import type { Workspace, Pane } from '../store.svelte'
/** /**
* Where a space was left: the folder, the tiles, the row, the pane. A tile * Where a space was left: the folder, the tiles, the row, the pane. A tile
* is the id of the node it shows, or the name of the view. `openId` is the * is the id of the node it shows; the name of a view is what a spot held
* focused tile's, all a spot held before there were tiles, and `open` the * while views were tiled among the documents, and is passed over now.
* documents alone, all it held before there were views. * `openId` is the focused tile's, all a spot held before there were tiles,
* and `open` the documents alone, all it held before there were views.
*/ */
type Spot = { type Spot = {
parentId: number parentId: number
@@ -144,47 +145,49 @@ export class Tree {
if (start) await this.enterSpace(start.id) if (start) await this.enterSpace(start.id)
} }
/** `views` false: the views are leaving with the focus, and are not left behind. */ /** Where this space is left, its documents on screen. */
#rememberSpot(views = true) { #rememberSpot() {
if (this.spaceId === null || this.parentId === null) return if (this.spaceId === null || this.parentId === null) return
// A card's back is the card under the board's cursor, which is not kept. const kept = this.ws.tiles.all.filter((tile) => tile.open)
const kept = this.ws.tiles.all.filter((tile) => !tile.empty && (tile.kind === 'node' || (views && tile.kind !== 'card')))
this.#spots[this.spaceId] = { this.#spots[this.spaceId] = {
parentId: this.parentId, parentId: this.parentId,
openId: this.ws.open?.id ?? null, openId: this.ws.open?.id ?? null,
cursor: this.cursor, cursor: this.cursor,
pane: this.ws.pane, pane: this.ws.pane,
tiles: kept.map((tile) => (tile.kind === 'node' ? tile.open!.id : tile.kind)), tiles: kept.map((tile) => tile.open!.id),
focused: Math.max(kept.indexOf(this.ws.tile), 0), focused: Math.max(kept.indexOf(this.ws.tile), 0),
} }
localStorage.setItem('diary:spots', JSON.stringify(this.#spots)) localStorage.setItem('diary:spots', JSON.stringify(this.#spots))
} }
async enterSpace(id: number) { async enterSpace(id: number) {
const leaving = this.spaceId !== null && this.spaceId !== id
/* /*
* The views look at every board and the whole diary, so with one of them * The views look at every board and the whole diary, so the plan screen
* focused they come along — moved, not copied — as the special workspace * stays as it is — a digit on the board shows that space's board — and
* once did: a digit on the board shows that space's board, and the * it is the documents behind it that are this space's.
* agenda stays beside it.
*/ */
let carried: Tile[] = [] const planning = this.ws.tiles.planning
let carriedFocus: Tile | undefined if (planning) this.ws.tiles.swap()
try {
await this.#enterSpace(id)
} finally {
if (planning) this.ws.tiles.swap()
}
await this.ws.tiles.loadViews()
}
async #enterSpace(id: number) {
const leaving = this.spaceId !== null && this.spaceId !== id
if (leaving) { if (leaving) {
await this.ws.drafts.flush() await this.ws.drafts.flush()
const carry = this.ws.focus !== 'list' && this.ws.focus !== 'node' this.#rememberSpot()
this.#rememberSpot(!carry)
const from = this.spaces.findIndex((s) => s.id === this.spaceId) const from = this.spaces.findIndex((s) => s.id === this.spaceId)
const to = this.spaces.findIndex((s) => s.id === id) const to = this.spaces.findIndex((s) => s.id === id)
this.ws.switchDirection = to >= from ? 1 : -1 this.ws.switchDirection = to >= from ? 1 : -1
if (carry) {
carried = this.ws.tiles.all.filter((tile) => tile.kind !== 'node' && tile.kind !== 'card')
carriedFocus = this.ws.tile.kind === 'card' ? (this.ws.tiles.view('board') ?? carried[0]) : this.ws.tile
}
// Each space has tiles of its own; these come back with this one. // Each space has tiles of its own; these come back with this one.
for (const tile of this.ws.tiles.all) tile.stopAutosave() for (const tile of this.ws.tiles.all) tile.stopAutosave()
this.ws.tiles.replace(carried, carriedFocus) this.ws.tiles.replace([])
this.ws.pane = carried.length > 0 ? 'document' : 'list' this.ws.pane = 'list'
} }
this.spaceId = id this.spaceId = id
localStorage.setItem('diary:space', String(id)) localStorage.setItem('diary:space', String(id))
@@ -204,12 +207,7 @@ export class Tree {
spot.tiles ?? spot.open ?? (spot.openId !== null && this.nodes.some((n) => n.id === spot.openId) ? [spot.openId] : []) spot.tiles ?? spot.open ?? (spot.openId !== null && this.nodes.some((n) => n.id === spot.openId) ? [spot.openId] : [])
const restored: Tile[] = [] const restored: Tile[] = []
for (const entry of entries) { for (const entry of entries) {
if (typeof entry === 'string') { if (typeof entry === 'string') continue
let tile = this.ws.tiles.view(entry)
if (!tile) this.ws.tiles.all.push((tile = new Tile(entry)))
restored.push(tile)
continue
}
const empty = this.ws.tiles.all.findIndex((tile) => tile.empty) const empty = this.ws.tiles.all.findIndex((tile) => tile.empty)
if (empty >= 0) this.ws.tiles.focused = empty if (empty >= 0) this.ws.tiles.focused = empty
else { else {
@@ -224,25 +222,23 @@ export class Tree {
} }
} }
this.ws.tiles.prune() this.ws.tiles.prune()
const back = carriedFocus ?? restored[Math.min(spot.focused ?? 0, restored.length - 1)] const back = restored[Math.min(spot.focused ?? 0, restored.length - 1)]
if (back && this.ws.tiles.all.includes(back)) { if (back && this.ws.tiles.all.includes(back)) {
this.ws.tiles.focusOn(this.ws.tiles.all.indexOf(back)) this.ws.tiles.focusOn(this.ws.tiles.all.indexOf(back))
if (!carriedFocus) this.ws.pane = spot.pane this.ws.pane = spot.pane
} else { } else {
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))
} }
} }
this.ws.tiles.trail = [] this.ws.tiles.trail = []
await this.ws.tiles.loadViews()
} }
/** Show the children of `id`, which must be a space or a folder. */ /** Show the children of `id`, which must be a space or a folder. */
async goTo(id: number, keep?: number) { async goTo(id: number, keep?: number) {
await this.ws.drafts.flush() await this.ws.drafts.flush()
const folder = await api.getNode(id) const folder = await api.getNode(id)
// Walking into another space by a link leaves this one where it was; the // Walking into another space by a link leaves this one where it was.
// views stay on screen, so they go along rather than stay behind too. if (this.spaceId !== null && folder.path[0]?.id !== this.spaceId) this.#rememberSpot()
if (this.spaceId !== null && folder.path[0]?.id !== this.spaceId) this.#rememberSpot(false)
this.parentId = id this.parentId = id
this.parentSealed = folder.sealed this.parentSealed = folder.sealed
this.query = '' this.query = ''
@@ -347,7 +343,14 @@ export class Tree {
} }
async openNode(id: number, edit = false) { async openNode(id: number, edit = false) {
if (this.ws.open?.id !== id) await this.ws.drafts.flushTile() /*
* The plan screen holds the views alone: what is opened from it — a
* card's notes, a day of the calendar, a search — opens on the documents'
* screen, beside what is there, and `S` goes back.
*/
const fromPlan = this.ws.tiles.planning
if (fromPlan) this.ws.tiles.leavePlan()
else if (this.ws.open?.id !== id) await this.ws.drafts.flushTile()
const node = await api.getNode(id) const node = await api.getNode(id)
if (isContainer(node)) { if (isContainer(node)) {
@@ -362,10 +365,7 @@ export class Tree {
if (edit && node.kind === 'document') this.ws.editing = true if (edit && node.kind === 'document') this.ws.editing = true
return return
} }
// A view is never put away by what is opened from it — a card's notes, if (fromPlan && !this.ws.tile.empty) this.ws.tiles.add()
// a day of the calendar, a search from the board: that opens beside it,
// and closing it goes back there.
if (this.ws.tile.kind !== 'node') this.ws.tiles.add()
this.ws.open = node this.ws.open = node
this.ws.draft = { name: node.name, body: node.body, created_at: node.created_at } this.ws.draft = { name: node.name, body: node.body, created_at: node.created_at }
@@ -549,7 +549,7 @@ export class Tree {
async deleteSpace(id: number) { async deleteSpace(id: number) {
const { removed } = await api.deleteNode(id) const { removed } = await api.deleteNode(id)
this.lastDeleted = id this.lastDeleted = id
for (const tile of this.ws.tiles.all) tile.clear() for (const tile of this.ws.tiles.every) if (tile.kind === 'node') tile.clear()
this.ws.tiles.prune() this.ws.tiles.prune()
this.ws.cards.rawBoard = null this.ws.cards.rawBoard = null
this.ws.cards.boardSpaceId = null this.ws.cards.boardSpaceId = null
@@ -585,7 +585,7 @@ export class Tree {
*/ */
async moveNodes(nodes: NodeSummary[], parent: number, where = 'here') { async moveNodes(nodes: NodeSummary[], parent: number, where = 'here') {
if (nodes.length === 0) return this.ws.say('nothing marked — press m on a row first', 'error') if (nodes.length === 0) return this.ws.say('nothing marked — press m on a row first', 'error')
const leaving = this.ws.tiles.all.filter((tile) => nodes.some((n) => n.id === tile.open?.id)) const leaving = this.ws.tiles.every.filter((tile) => nodes.some((n) => n.id === tile.open?.id))
for (const tile of leaving) { for (const tile of leaving) {
await this.ws.drafts.flushTile(tile) await this.ws.drafts.flushTile(tile)
tile.clear() tile.clear()
+1 -1
View File
@@ -51,6 +51,6 @@ export class Vault {
failed > 0 || shared > 0 ? 'error' : undefined, failed > 0 || shared > 0 ? 'error' : undefined,
) )
await this.ws.tree.refresh() await this.ws.tree.refresh()
for (const tile of this.ws.tiles.all) if (tile.open?.kind === 'file') tile.open = await api.getNode(tile.open.id) for (const tile of this.ws.tiles.every) if (tile.open?.kind === 'file') tile.open = await api.getNode(tile.open.id)
} }
} }
+3 -2
View File
@@ -160,7 +160,7 @@ export class Workspace {
// the draft included, so pick up where it stopped rather than start over. // the draft included, so pick up where it stopped rather than start over.
if (this.tree.spaces.length > 0) { if (this.tree.spaces.length > 0) {
await this.tree.refresh() await this.tree.refresh()
for (const tile of this.tiles.all) { for (const tile of this.tiles.every) {
if (tile.open && tile.dirty) await this.guard(() => this.drafts.save({ quiet: true, tile })) if (tile.open && tile.dirty) await this.guard(() => this.drafts.save({ quiet: true, tile }))
} }
if (this.drafts.writing?.dirty) await this.guard(() => this.drafts.saveWriting()) if (this.drafts.writing?.dirty) await this.guard(() => this.drafts.saveWriting())
@@ -178,7 +178,8 @@ export class Workspace {
this.vault.status = null this.vault.status = null
this.tree.spaces = [] this.tree.spaces = []
this.tree.nodes = [] this.tree.nodes = []
for (const tile of this.tiles.all) tile.clear() if (this.tiles.planning) this.tiles.leavePlan()
for (const tile of this.tiles.every) tile.clear()
this.tiles.prune() this.tiles.prune()
} }
+2
View File
@@ -24,6 +24,8 @@ export class Writing {
dirty = $state(false) dirty = $state(false)
/** The text was found in this browser, newer than what the server has. */ /** The text was found in this browser, newer than what the server has. */
readonly recovered: boolean readonly recovered: boolean
/** Opened to be typed into straight away: the editor starts in insert mode, at the end. */
append = false
#write: (text: string) => Promise<void> #write: (text: string) => Promise<void>
#fail: (error: unknown, retry: number) => void #fail: (error: unknown, retry: number) => void