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 |
| `w` | overview of every space; `hjkl` then `Enter` |
| `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 |
| `Alt-Shift-h/j/k/l` | move the focused tile that way |
| `Ctrl-h/j/k/l` | focus the tile in that direction, from the editor too; `Ctrl-h` past the leftmost tile focuses the list |
| `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 |
| `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 |
| `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 |
@@ -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 |
| `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 |
| `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 |
| `b` | the board of this space, in a tile; 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 |
| `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, 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, on the plan screen |
| `T` | today's diary entry, started if there is none |
| `C` | the diary by month (`:cal`) — see [The diary](#the-diary) |
| `[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 |
| `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 |
| `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 |
@@ -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
rather than opening it twice.
`Alt-h/j/k/l` moves the focus between tiles and `Alt-Shift-h/j/k/l` moves the
tile itself; `gt` / `gT` and `:bn` / `:bp` cycle through them, `:ls` lists
them, `:b <n or name>` focuses one, `:close` (`Alt-q`) closes one and `:only`
every other. `:next` and `:prev` still walk the list. Each space remembers its
tiles, so switching away and back finds them as they were. A card's notes
open in a tile beside what was being read, and `q` there goes back to the
board. Under 768px wide only the focused tile is shown.
`Ctrl-h/j/k/l` moves the focus between tiles, from the editor too, and
`Ctrl-Shift-h/j/k/l` moves the tile itself; while reading, `h` and `l` go to
the tile left and right. `gt` / `gT` and `:bn` / `:bp` cycle through them,
`:ls` lists them, `:b <n or name>` focuses one, `:close` (`q` while reading)
closes one and `:only` every other. `:next` and `:prev` still walk the list.
Each space remembers its tiles, so switching away and back finds them as they
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
@@ -257,8 +267,8 @@ start as `sftp_host_key` in the data directory.
## Boards
`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`
puts it up with the agenda and the calendar. Every space has one except the
being browsed on the plan screen, beside the agenda and the calendar, and `q`
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
that for the space you are in (the dimmed board door on the rail does the
same). A new board starts with `backlog`, `doing` and `done`, and the columns mean
+23 -26
View File
@@ -3,42 +3,39 @@
## Views
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
screen and walked the same way. `S` (or `:plan`) puts the board, the agenda
and the calendar up together, ahead of the documents with the board focused —
the board on the left, the agenda over the calendar on the right when nothing
else is open — and puts every view away again.
a card are views, and they have a screen of their own: the plan. `S` (or
`:plan`) goes there — the board on the left, the agenda over the calendar on
the right, and no list — and back to the documents, which wait behind it as
they were left. The views are tiles like the documents', walked the same way.
![The board, the agenda and the calendar](../images/board.png)
| key | what it does |
| --- | --- |
| `S` | the board, the agenda and the calendar up together, or every view away |
| `b`, `t`, `C` | to the board, the agenda or the calendar, opening its tile when it is not up |
| `S` | to the plan screen, or back to the documents |
| `b`, `t`, `C` | to the board, the agenda or the calendar, on the plan screen |
| `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 |
| `f` | the focused tile fills the screen; again or `Esc` to leave |
| `1` … `9` | switch space; with a view focused, the views come along |
| `q`, `Esc` | close the view's tile, back to the tile focused before it |
| `1` … `9` | switch space; the plan stays, and the board shows that space's |
| `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
lit in its head; the other views keep their cursors, dimmed. A view is never
replaced by what is opened from it — a card's notes, a day of the calendar, an
entry of the agenda, a row of the list: that opens in a tile beside it, and
`q` on it comes back to the view. Each view is read again whenever it is gone
to.
lit in its head; the other views keep their cursors, dimmed. What is opened
from a view — a card's notes, a day of the calendar, an entry of the agenda —
is a document, so it opens on the documents' screen, beside what is there, and
`S` comes back to the plan. Each view is read again whenever it is gone to.
Each space keeps its own tiles, views among them. Switching space with a view
focused takes the board, the agenda and the calendar along, the way the
special workspace used to stay up: a digit on the board shows that space's
board, with the agenda still beside it. Switching with a document or the list
focused leaves them in the space, and they are there again on coming back.
The plan is the same in every space, the way the desktop's special workspace
stays up: a digit on the board shows that space's board, with the agenda still
beside it. Each space keeps its own documents behind it, and they are there
again on going back.
## The board
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
the diary, the last board shown. The head of the board names every space that
has one, with the digit that goes there.
@@ -119,8 +116,8 @@ outside the browser, see [Capture](Capture.md).
### Notes
`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
back to the board, on the same card.
root of the space the first time, among the documents. `S` goes back to the
board, on the same card.
The link is loose on purpose: deleting the document leaves the card, because the
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
`t` (or `:agenda`) gathers what needs you from every board at once, in a tile
of its own.
`t` (or `:agenda`) gathers what needs you from every board at once, on the
plan screen.
![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 |
| `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 |
| `D` | due date |
| `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 |
| `w` | overview of every space — hjkl, then Enter |
| `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 |
| `Alt-Shift-h/j/k/l` | move the focused tile that way |
| `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 |
| `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 |
| `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 |
| `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 |
@@ -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 |
| `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 board, the agenda and the calendar, tiled ahead of what is open with the board focused — S again puts every view away |
| `t` | agenda — late, due this week, in hand, on every board — in a tile of its own |
| `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 — on the plan screen |
| `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 |
| `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 |
| `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 |
@@ -64,7 +63,7 @@ arguments and `↑` / `↓` recall earlier lines.
| `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 |
| `,` | 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
@@ -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 |
| `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 |
| `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 |
| `D` | due date: fri, tm, +3d, 10-31 … |
| `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 |
| `R · D` | rename the card, its due date |
| `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
@@ -123,6 +122,7 @@ The first list is the inbox, the last list is done.
| key | what it does |
| --- | --- |
| `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 |
| `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 |
@@ -154,8 +154,8 @@ whole workspace. A command that does a second thing takes a word for it:
| command | what it does |
| --- | --- |
| `:board` | this space's board, in a tile — 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 |
| `:board` | this space's board, on the plan screen — the last one shown when this space has none |
| `: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 |
| `: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 |
@@ -196,7 +196,7 @@ whole workspace. A command that does a second thing takes a word for it:
| 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 |
| `: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 |
+5 -5
View File
@@ -13,9 +13,9 @@ an entry written after midnight on a trip still lands on the right day.
## The calendar
`C` (or `:cal`) shows the diary a month at a time, in a
[tile of its own](Boards-and-Agenda.md#views) — under the agenda when `S` puts
them up together.
`C` (or `:cal`) shows the diary a month at a time, on the
[plan screen](Boards-and-Agenda.md#views), under the agenda. The month is
fitted into its tile rather than scrolled.
![The diary by month](../images/calendar.png)
@@ -25,8 +25,8 @@ them up together.
| `j` / `k` | a week forward / back |
| `H` / `L` | a month back / forward |
| `.` | today |
| `Enter` | open the day's entry beside the calendar, or start one dated that day |
| `q`, `Esc` | close the calendar's tile; `q` on an entry opened from it comes back here |
| `Enter` | open the day's entry among the documents, or start one dated that day; `S` comes back |
| `q`, `Esc` | back to the documents |
A dot marks a day with an entry. A number says how many when there are more
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
`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 in a tile, and
`Esc` to close it. `S` puts it up with the agenda and the calendar, and puts
them away again; `Alt-h`/`j`/`k`/`l` walk between the tiles.
inbox of this space's board. Press `b` to see the board on the plan screen,
beside the agenda and the calendar, and `Esc` to go back to the documents.
`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.
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
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
beside what is open; `S` puts it up with the board and the calendar, and puts
them away again, and `Alt-h`/`j`/`k`/`l` walk the tiles.
when anything is late or due. Any other time, press `t`. It is on the plan
screen, with the board and the calendar, in place of the documents; `S` goes
there and back again, and `Ctrl-h`/`j`/`k`/`l` walk the tiles.
![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)
`Alt-h` and `Alt-l` move between the tiles and back to the list, `Alt-q`
closes one, and `f` makes the focused one fill the screen. Each space remembers
`h` and `l` move between the tiles and back to the list, `Ctrl-h` and
`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
[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
document, a card, or more of a document.
Press `b` to see the **board**, in a tile beside the document. A digit there
shows the board of another space.
Press `b` to see the **board**, on the plan screen. A digit there shows the
board of another space.
![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 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,
also beside, and `q` on it 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
its tile.
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 goes
back to the documents.
### 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.
- Tiles share the space the way a tiling window manager does: two side by side,
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
goes back to the list. `Alt-Shift-h/j/k/l` moves the tile itself.
- `Ctrl-h/j/k/l` moves the focus between tiles, from the editor too, and
`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
`: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.
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
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
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.
| 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]`) |
| `dd` | drop the note (asks y/N) |
| `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
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('global', 'space'), 'switch space, Tab cycles'],
[keysOf('global', 'overview'), 'overview of every space'],
[keysOf('global', 'plan'), 'board, agenda and calendar'],
[[...keysOf('global', 'board'), ...keysOf('global', 'agenda'), ...keysOf('global', 'calendar')], 'one of them, in a tile'],
[keysOf('global', 'plan'), 'the plan: board, agenda, calendar'],
[[...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('global', 'find'), 'find in every space'],
[keysOf('tree', 'search'), 'search this space'],
@@ -293,6 +293,7 @@
class="panes switch"
class:reading={workspace.tiles.all.some((tile) => !tile.empty)}
class:full={workspace.fullscreen}
class:plan={workspace.tiles.planning}
class:focus-doc={workspace.focus !== 'list'}
style="--dir: {workspace.switchDirection}"
>
@@ -433,6 +434,10 @@
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. */
.panes.full { grid-template-columns: 1fr; }
.panes.full.focus-doc .window.list,
+17 -7
View File
@@ -83,14 +83,18 @@
</div>
<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 {
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
height: 100%;
overflow-y: auto;
padding: 20px 24px;
overflow: hidden;
padding: 16px 20px;
}
header { display: flex; align-items: center; gap: 12px; }
@@ -99,21 +103,26 @@
header .title { color: var(--light); }
.grid {
flex: 0 1 auto;
min-height: 0;
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
grid-template-rows: auto;
grid-auto-rows: minmax(0, 40px);
gap: 3px;
list-style: none;
margin: 0;
padding: 0;
}
.weekday { text-align: center; font-size: 10px; color: var(--muted); }
.grid > li { min-height: 0; }
.day {
position: relative;
display: grid;
place-items: center;
width: 100%;
aspect-ratio: 1.4;
height: 100%;
padding: 0;
border: 1px solid transparent;
border-radius: var(--radius-s);
@@ -126,12 +135,13 @@
.day.written { color: var(--fg); }
.day.today { border-color: var(--rim); }
.day.at { background: var(--light); color: var(--ink); }
.dot, .many { position: absolute; bottom: 4px; }
.dot, .many { position: absolute; bottom: 3px; }
.dot { width: 5px; height: 5px; border-radius: 999px; background: var(--light); }
.many { font-size: 9px; color: var(--light); }
.day.at :is(.dot) { background: var(--ink); }
.day.at .many { color: var(--ink); }
.chosen { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.chosen h2 {
display: flex;
align-items: center;
+9 -2
View File
@@ -155,6 +155,7 @@
import { onDestroy, onMount } from 'svelte'
import { api } from './api'
import { hooks } from './commands'
import { claim } from './modes'
import { editorTheme, highlighting, languages } from './editorTheme'
import { embedSnippet } from './markdown'
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
* `: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) {
if (claim(event)) return
if (event.key !== 'Escape' || (workspace.vimEnabled && mode !== 'normal')) return
if (view && completionStatus(view.state) !== null) return
event.preventDefault()
@@ -317,6 +320,7 @@
parent: host!,
})
if (writing?.append) view.dispatch({ selection: { anchor: text.length } })
if (writing || workspace.tile === tile) view.focus()
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,
// which is there to be written; anything else starts in normal mode,
// 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
Vim.handleKey(cm, 'i', 'workspace')
}
+18 -13
View File
@@ -83,6 +83,7 @@
<footer class="label">
<span>writing — it saves itself</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>
</footer>
{:else}
@@ -92,31 +93,35 @@
<style>
/*
* Hung from under the bar across the frame's opening, as the desktop's
* scratchpad slides down from under its bar: hull, a rim, the lower corners
* rounded, and the light along its lower edge to say it has the keys.
* The upper part of the porthole, come down over what is in it: the same
* opening under the bar, the same rounded corners, and the focused
* 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 {
--pane-bg: color-mix(in oklab, var(--surface) 40%, var(--hull));
position: absolute;
top: calc(var(--bar-h) + var(--frame));
left: calc(var(--frame) + 24px);
right: calc(var(--rail-w) + 24px);
top: var(--bar-h);
left: var(--frame);
right: var(--rail-w);
z-index: 25;
height: min(62vh, 640px);
height: min(62vh, 640px, calc(100% - var(--bar-h) - var(--status-h)));
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 20px 12px;
background: var(--hull);
border: 1px solid var(--surface-alt);
border-top: 0;
border-bottom-color: var(--light-dim);
border-radius: 0 0 var(--frame-radius) var(--frame-radius);
border: 1px solid transparent;
border-radius: var(--frame-radius);
background:
linear-gradient(var(--pane-bg), var(--pane-bg)) padding-box,
linear-gradient(135deg, var(--light), var(--light-dim)) border-box;
box-shadow: 0 18px 40px color-mix(in oklab, var(--ink) 70%, transparent);
animation: lower var(--t-slow) var(--ease-snap) both;
}
@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; }
+1 -1
View File
@@ -135,7 +135,7 @@
actions(index)
}}
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:nested
data-index={index}
+7 -1
View File
@@ -7,7 +7,13 @@
const { pending = '' }: { pending?: string } = $props()
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.
+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
* 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)
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
}
if (!shift) return void focusTile(next)
@@ -103,15 +104,24 @@ function tileKey(direction: Direction, shift: boolean) {
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> } = {
always: {
save: () => {
if (workspace.drafts.writing) guard(() => workspace.drafts.saveWriting())
else if (workspace.open) guard(() => workspace.drafts.save())
},
close: () => void runCommand('close'),
focus: (key) => tileKey(key.slice(-1) as Direction, false),
swap: (key) => tileKey(key.slice(-1) as Direction, true),
// Under the drawer the tiles are out of sight, so the keys are the note's.
focus: (key) => (workspace.scratch.open ? false : tileKey(key.slice(-1) as Direction, false)),
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: {
@@ -318,7 +328,7 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
},
line: () => line(':'),
help: () => void runCommand('help'),
raise: () => void workspace.scratch.close(),
raise: () => guard(raise),
nothing: () => {},
},
@@ -355,6 +365,8 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
open: (key) => {
const enter = key === 'Enter' || key === 'Shift-Enter'
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.
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)
@@ -364,10 +376,10 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
beside: () => {
if (!reading()) guard(() => workspace.tiles.openBeside())
},
// Leftwards at every depth: from the document to the list, from the
// list up a level.
// Leftwards at every depth: from a document to the tile left of it, from
// the leftmost to the list, from the list up a level.
left: () => {
if (workspace.open && workspace.pane === 'document') focusPane('list')
if (workspace.open && workspace.pane === 'document') tileKey('h', false)
else guard(() => workspace.tree.up())
},
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()),
},
/** 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: {
close: () => void runCommand('close'),
},
@@ -549,9 +561,9 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
agenda: () => void runCommand('agenda'),
today: () => void runCommand('today'),
calendar: () => void runCommand('cal'),
// `S`, as the desktop's special workspace is bound: the board, the
// agenda and the calendar, put up and away again. `b`, `t` and `C` only
// ever go to their tile.
// `S`, as the desktop's special workspace is bound: the plan screen —
// the board, the agenda and the calendar — and back to the documents.
// `b`, `t` and `C` go there too, to their own view.
plan: () => void runCommand('plan'),
line: () => line(':'),
help: () => void runCommand('help'),
+3 -3
View File
@@ -591,14 +591,14 @@ export const commands: CommandSpec[] = [
{
name: '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')),
},
{
name: 'plan',
aliases: ['special'],
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()),
},
{
@@ -874,7 +874,7 @@ export const commands: CommandSpec[] = [
name: 'close',
aliases: ['clo'],
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: () =>
workspace.guard(async () => {
await workspace.drafts.flushTile()
+11 -11
View File
@@ -46,7 +46,7 @@ export interface Binding {
context: Context
/**
* 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
* 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: ['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: '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: ['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: '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: ['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: 'global', keys: ['gt'], action: 'nextTile', label: 'gt / gT', help: 'next / previous tile — a view too' },
{ 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: ['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' },
@@ -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: ['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: '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: ['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: ['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 — 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: ['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: '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: ['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: ['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' },
@@ -136,12 +135,13 @@ export const bindings = [
// --- the workspace around the panes ------------------------------------------
{ 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: '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
// hands the focus back to it instead of walking the list unseen.
{ context: 'writing', keys: ['*'], action: 'refocus' },
// --- 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 ---------------------------------------------------------------
{ 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: ['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: ['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: ['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' },
@@ -183,7 +183,7 @@ export const bindings = [
{ 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: ['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' },
// --- 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
* 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
* drawer the scratchpad, while it is down
* 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`,
* `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
* 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.
@@ -134,3 +134,19 @@ export function dispatch(event: KeyboardEvent, pending: string, line: boolean, t
}
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. */
async #reloadCardsAbout() {
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
if (id === null) return
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. */
watchConnection() {
window.addEventListener('online', () => {
for (const tile of this.ws.tiles.all) {
for (const tile of this.ws.tiles.every) {
if (!tile.dirty) continue
tile.stopAutosave()
void this.#writeBack(tile)
@@ -107,7 +107,7 @@ export class Drafts {
* unsaved work without a word.
*/
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. */
@@ -173,7 +173,7 @@ export class Drafts {
* then refused, and `:e!` takes the rewritten text.
*/
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
const node = await api.getNode(tile.open.id).catch(() => null)
if (!node) continue
@@ -236,7 +236,7 @@ export class Drafts {
/** Every tile showing `id` reads it again, after it was written somewhere else. */
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
const node = await api.getNode(id)
tile.open = node
+12
View File
@@ -43,6 +43,18 @@ export class Scratchpad {
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. */
newNote() {
this.#writeNote(null, '')
+70 -40
View File
@@ -1,6 +1,6 @@
import { isContainer, type WorkspaceNode } from '../api'
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
@@ -68,9 +68,10 @@ export class Tile {
}
/**
* The tiles on screen and which has the focus: opened, closed, walked, and
* the views — the board, the agenda, the calendar, a card's back — put up
* among them.
* The tiles on screen and which has the focus: opened, closed, walked. There
* are two screens of them, and one is shown at a time: the documents, beside
* 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 {
readonly ws: Workspace
@@ -91,9 +92,19 @@ export class Tiles {
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[] {
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'
}
/** `: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() {
if (this.planning && this.current.kind !== 'card') return this.leavePlan()
this.ws.drafts.leaveCard()
this.current.clear()
this.prune()
@@ -201,46 +217,60 @@ export class Tiles {
/*
* 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
* gone to: it is looked at in passing, and what it says should be what is
* true now.
* the tiles of the plan screen. Each is read again whenever it is gone to:
* it is looked at in passing, and what it says should be what is 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) {
await this.loadView(kind)
if (!this.view(kind)) {
// The one empty tile there can be takes it; otherwise it goes beside.
if (this.current.empty) this.current.kind = kind
else this.all.push(new Tile(kind))
}
// Coming to the plan, every view on it is read again, not only this one.
const kinds = this.planning ? [kind] : Tiles.PLAN
await Promise.all(kinds.map((each) => this.loadView(each)))
this.enterPlan()
if (!this.view(kind)) this.all.push(new Tile(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')
}
/** The views `S` puts up together, in the order they are tiled. */
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.
*/
/** `S`: the plan screen with the board focused, or back to the documents. */
async togglePlan() {
if (Tiles.PLAN.every((kind) => this.view(kind))) {
for (const tile of this.all) if (tile.kind !== 'node') tile.clear()
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')
if (this.planning) return this.leavePlan()
await this.show('board')
}
/** 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() {
if (this.view('board') || this.view('card')) await this.ws.cards.loadBoard()
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
* is the id of the node it shows, or the name of the view. `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.
* is the id of the node it shows; the name of a view is what a spot held
* while views were tiled among the documents, and is passed over now.
* `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 = {
parentId: number
@@ -144,47 +145,49 @@ export class Tree {
if (start) await this.enterSpace(start.id)
}
/** `views` false: the views are leaving with the focus, and are not left behind. */
#rememberSpot(views = true) {
/** Where this space is left, its documents on screen. */
#rememberSpot() {
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.empty && (tile.kind === 'node' || (views && tile.kind !== 'card')))
const kept = this.ws.tiles.all.filter((tile) => tile.open)
this.#spots[this.spaceId] = {
parentId: this.parentId,
openId: this.ws.open?.id ?? null,
cursor: this.cursor,
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),
}
localStorage.setItem('diary:spots', JSON.stringify(this.#spots))
}
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
* focused they come along — moved, not copied — as the special workspace
* once did: a digit on the board shows that space's board, and the
* agenda stays beside it.
* The views look at every board and the whole diary, so the plan screen
* stays as it is — a digit on the board shows that space's board — and
* it is the documents behind it that are this space's.
*/
let carried: Tile[] = []
let carriedFocus: Tile | undefined
const planning = this.ws.tiles.planning
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) {
await this.ws.drafts.flush()
const carry = this.ws.focus !== 'list' && this.ws.focus !== 'node'
this.#rememberSpot(!carry)
this.#rememberSpot()
const from = this.spaces.findIndex((s) => s.id === this.spaceId)
const to = this.spaces.findIndex((s) => s.id === id)
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.
for (const tile of this.ws.tiles.all) tile.stopAutosave()
this.ws.tiles.replace(carried, carriedFocus)
this.ws.pane = carried.length > 0 ? 'document' : 'list'
this.ws.tiles.replace([])
this.ws.pane = 'list'
}
this.spaceId = 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] : [])
const restored: Tile[] = []
for (const entry of entries) {
if (typeof entry === 'string') {
let tile = this.ws.tiles.view(entry)
if (!tile) this.ws.tiles.all.push((tile = new Tile(entry)))
restored.push(tile)
continue
}
if (typeof entry === 'string') continue
const empty = this.ws.tiles.all.findIndex((tile) => tile.empty)
if (empty >= 0) this.ws.tiles.focused = empty
else {
@@ -224,25 +222,23 @@ export class Tree {
}
}
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)) {
this.ws.tiles.focusOn(this.ws.tiles.all.indexOf(back))
if (!carriedFocus) this.ws.pane = spot.pane
this.ws.pane = spot.pane
} else {
this.cursor = Math.min(spot.cursor, Math.max(this.nodes.length - 1, 0))
}
}
this.ws.tiles.trail = []
await this.ws.tiles.loadViews()
}
/** Show the children of `id`, which must be a space or a folder. */
async goTo(id: number, keep?: number) {
await this.ws.drafts.flush()
const folder = await api.getNode(id)
// Walking into another space by a link leaves this one where it was; the
// 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(false)
// Walking into another space by a link leaves this one where it was.
if (this.spaceId !== null && folder.path[0]?.id !== this.spaceId) this.#rememberSpot()
this.parentId = id
this.parentSealed = folder.sealed
this.query = ''
@@ -347,7 +343,14 @@ export class Tree {
}
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)
if (isContainer(node)) {
@@ -362,10 +365,7 @@ export class Tree {
if (edit && node.kind === 'document') this.ws.editing = true
return
}
// A view is never put away by what is opened from it — a card's notes,
// 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()
if (fromPlan && !this.ws.tile.empty) this.ws.tiles.add()
this.ws.open = node
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) {
const { removed } = await api.deleteNode(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.cards.rawBoard = null
this.ws.cards.boardSpaceId = null
@@ -585,7 +585,7 @@ export class Tree {
*/
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')
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) {
await this.ws.drafts.flushTile(tile)
tile.clear()
+1 -1
View File
@@ -51,6 +51,6 @@ export class Vault {
failed > 0 || shared > 0 ? 'error' : undefined,
)
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.
if (this.tree.spaces.length > 0) {
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 (this.drafts.writing?.dirty) await this.guard(() => this.drafts.saveWriting())
@@ -178,7 +178,8 @@ export class Workspace {
this.vault.status = null
this.tree.spaces = []
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()
}
+2
View File
@@ -24,6 +24,8 @@ export class Writing {
dirty = $state(false)
/** The text was found in this browser, newer than what the server has. */
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>
#fail: (error: unknown, retry: number) => void