diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 20:33:59 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 20:33:59 +0200 |
| commit | aa190a4775d99f205ef29a6c99ec7ab283c76ee8 (patch) | |
| tree | d28cb2cb1a01623dfb24854313cb6c7c3a224768 /docs/superpowers/specs | |
| parent | 2537315f53cb6d0dbc5238245451e85b388ae5c5 (diff) | |
| download | quickshell-aa190a4775d99f205ef29a6c99ec7ab283c76ee8.tar.gz quickshell-aa190a4775d99f205ef29a6c99ec7ab283c76ee8.zip | |
docs: notes drawer module spec and plan
A drawer tile and page for bash-notes: browse, write, edit and delete
notes in the page, sharing notes.sh's store so both tools work on the
same notes.
Git follows notes.sh's shape, pull before write, commit and push after,
with two departures the page needs: an unconfirmed Save leaves db.json
uncommitted (write and commit are two clicks here, one invocation there),
so the pulls carry --autostash, and lastpull is stamped after the pull
rather than before it, which notes.sh does and then ignores the failed
pull that follows.
Diffstat (limited to 'docs/superpowers/specs')
| -rw-r--r-- | docs/superpowers/specs/2026-10-16-notes-design.md | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-10-16-notes-design.md b/docs/superpowers/specs/2026-10-16-notes-design.md new file mode 100644 index 0000000..9b460a8 --- /dev/null +++ b/docs/superpowers/specs/2026-10-16-notes-design.md @@ -0,0 +1,236 @@ +# Notes module + +A drawer tile and page for `bash-notes`, the shell note taker. Listing, +writing, editing and deleting all happen in the page; the store is the same +one `notes.sh` uses, so both tools work on the same notes. Git sync moves +from "run `notes.sh` and it pushes for you" to a confirm on save. + +The predecessor is `rofi-notes.sh`, which lists notes in rofi and opens one +in a terminal editor. It can list and choose, but writing and reading both +leave the desktop, and nothing shows a note without a pager. + +## The store + +`notes.sh` keeps everything under `BASEDIR`, default +`~/.local/share/bash-notes`: + +- **`db.json`** — the index. `{"params": {...}, "git": {"lastpull": ...}, + "notes": [{"id": "1", "title": "...", "file": "1750000000"}]}`. +- **`notes/`** — one file per note, named by the creation time in epoch + seconds. The file holds the body; the title is only in `db.json`. + +`id`, `title` and `file` are all **strings** in `db.json`. `notes.sh` writes +them with `jq --arg` and looks a note up with `select(.id == $i)`, comparing +string to string, so a number where a string belongs would make the CLI stop +finding a note the page wrote. The page matches: `id` and `file` are written +as strings. + +`notes.sh` lists by iterating `notes/*` and looking each filename up in +`db.json` for its title. The page lists the other way round, from +`db.json.notes`, which is where the titles are; a file with no entry would +not show, and an entry with no file shows an error when its body is read. +The two stay in step because every page write touches both. + +A write replaces only `.notes`; `params` and the rest of `git` round-trip +unchanged, so a store the page writes before `notes.sh` has ever run still has +the shape `firstrun` gives it. `git.lastpull` is the one exception, set +deliberately on a pull (see Git); it is what stops `notes.sh` pulling on every +run after the page has pulled. + +## Configuration + +Paths and git settings come from `notes.sh`'s own config, not from constants, +so a changed `BASEDIR` moves the page with it. At load the module runs one +shell, sourcing the rc and printing what it needs: + + sh -c '[ -r "$HOME/.config/bash-notes.rc" ] && . "$HOME/.config/bash-notes.rc" + base="${BASEDIR:-$HOME/.local/share/bash-notes}" + printf ... "$base" "${DB:-$base/db.json}" "${NOTESDIR:-$base/notes}" ...' + +Sourcing is what makes `${BASEDIR}` in the rc expand the way `notes.sh` +expands it, `~` included. Git is enabled when `USEGIT` is `true` and +`GITREMOTE` is non-empty, the same test `notes.sh` makes. `GITREMOTE`'s +value is never read by the page: the remote is already configured in the +notes repository, so `git push` needs only the directory. `GITCLIENT` is +read for the commit message. + +## Module + +`desktop/modules/notes/NotesModule.qml`, name `notes`, `alwaysActive: true`. + +- **Icon**: `\uf249`, `sticky-note`, present in Inconsolata Nerd Font. +- **Tile line**: the note count, `12 notes`, from `db.json`. The module holds + a `FileView` on `db.json` with `watchChanges`, so the count is right the + moment the grid appears and moves when a note is added from either side. + That is a watch held while the drawer is closed, which the other pages + avoid; here it is the tile's whole content and it is one small file, the + same call mail makes for its unread count. +- **Page**: a `Page` hosting `NotesPage`, `owner: mod`. + +## Page + +`desktop/modules/notes/NotesPage.qml`, UI only, in two states. + +### List + +- A search box, a **New note** button and, when git is on, an **Update list** + button. +- Newest first, each row a title and its date (`dd/mm/yyyy hh:mm`, from the + `file` timestamp, local time). A click opens the note. Case-insensitive + substring search on the title, the same filter the clipboard page uses. +- **Update list** runs the page's pull (`pull --rebase --autostash`, see Git) + and stamps `git.lastpull`, so `notes.sh -l` does not immediately pull + again. The file watch then re-reads `db.json`, and notes pulled from another + machine appear. A failed pull shows git's reason in a line under the button. + +### Editor + +Opened by a row (existing note) or **New note**. + +- A title field and a body, with an **Edit / Preview** toggle. Preview + renders the body as Markdown; Edit shows the raw Markdown in a `TextEdit`. + An existing note opens in Preview, a new one in Edit. +- **Save** is two clicks, so the note is on disk before git is involved: the + first writes it and the button becomes **Commit & push?**; the second runs + the git half (see Git) and returns to the list. Leaving with the second click + unmade keeps the saved note and does not push it. With git off there is no + git half, so Save writes and returns to the list in one click. +- **Cancel** returns to the list without saving; **Delete**, existing notes + only, is two clicks the same way and pushes the removal. +- **Unsaved text is discarded.** Leaving the editor with Cancel, the page's own + back arrow, Escape or a click outside the drawer drops whatever was typed and + not saved. `Page` has no veto over a back, and a drawer that destroys its page + is the clipboard page's own ethos; the save is the explicit act. +- Body `TextEdit`s take the visible height and scroll themselves, as + `ClipboardPage` does, so the `Page`'s outer `ScrollView` never scrolls. + +## Writes + +All index writes go through the module's `db.json` `FileView` with +`atomicWrites: true`, so a write that dies mid-way cannot leave a half +`db.json` for `notes.sh` to trip on. A body is written with a `FileView` on +its own path, which creates the file. + +- **Create**: the first click pulls first (see Git), then re-reads `db.json` + so the append is based on what the pull brought in, not on what was read + before it; `notes.sh` reads the id after its own pull for the same reason. + `file` is the current time in epoch seconds, `id` is the highest existing id + plus one (as a string), and `lastpull` is set to now in the same write. + Write the body to `notes/<file>`, append `{id, title, file}` to + `db.json.notes`. +- **Edit**: replace the title in the entry, rewrite `notes/<file>`. +- **Delete**: drop the entry from `db.json.notes` and `rm -- notes/<file>`. + `FileView` has no delete, so this is the one `execDetached` filesystem + call. + +A save is committed only on the second click, so a note written and not +confirmed stays uncommitted, and so does the `lastpull` stamp **Update list** +writes. That is why the page's pulls carry `--autostash`. + +An id and a `file` colliding with an existing note is not guarded against: +`notes.sh` takes `date +%s` once per run and a run is interactive, so two +notes in one second are as unlikely here as there, and a collision would +overwrite the earlier note's file. `shortcut: second-granularity filenames, +upgrade to a fresh second or a suffix if a collision is ever seen.` + +## Git + +Every git action shells out and shows git's stderr when it fails; the page +never parses git's output. + +The order is `notes.sh`'s: pull before the write, then add, commit, push. The +pull happens on the first click, before anything is written, so it runs on a +clean tree and never needs a rebase. The commit and the push happen on the +second click. + +- **Create**: click 1 `pull --rebase --autostash`, then the write; click 2 + `git add .`, `commit`, `git push origin HEAD`. +- **Edit / Delete**: click 1 writes; click 2 `git add .`, `commit`, + `git push origin HEAD`. `notes.sh` pulls on add only, and the page matches. +- **Update list**: `pull --rebase --autostash`, then stamp `git.lastpull`. + +`--autostash` is there because the page's write and commit are two clicks, so +an unconfirmed save leaves `db.json` uncommitted, and this machine's +`pull.rebase=true` refuses a pull over any unstaged change. With the second +click made, the tree is clean and it does nothing. `notes.sh` has no such +gap: its write and its commit are one invocation, so its tree is always clean +when it pulls. + +The commands run as one `sh -c` chain with the base directory and the message +passed as `$1` and `$2`, never built into the command string, the rule the +clipboard page keeps for its own untrusted text. `git add .` runs from +`BASEDIR`, as `notes.sh` runs it. A second click with nothing staged, an edit +that changed nothing, is not a failure: git has nothing to record and the push +still runs. + +`git.lastpull` is stamped after the pull, not before. `notes.sh` stamps first +and then pulls, which leaves its own `db.json` dirty for the pull it is about +to run, and it ignores the resulting failure; stamping on return keeps the +field truthful without that. The field is what `notes.sh -l` reads to decide +whether to pull again inside `GITSYNCDELAY`, so a page pull that did not stamp +would make every CLI list pull again. + +The commit message follows `notes.sh`'s wording but names the page, so +history says where a change came from: `quickshell - adding note from +<client>`, `quickshell - <client> note edited.`, `quickshell - <client> +removing note ID <id>.` (`<client>` is `GITCLIENT`, defaulting to the +hostname, as in `notes.sh`.) Flip the prefix to `notes.sh - ` if identical +messages matter more than the origin. + +Nothing pulls on open, so the list shows local state until **Update list** is +pressed. `notes.sh`'s own `list` pulls on a timer; the drawer makes it an +explicit click instead. + +## Markdown + +Preview is a read-only `TextEdit` with `textFormat: TextEdit.MarkdownText`, +selectable as the assistant canvas is. Qt's Markdown is a subset: headings, +emphasis, lists, links, inline code and quotes render; tables and fenced-code +highlighting do not. Links open externally, restricted to `http`, `https` +and `mailto` as `Segments.qml` does. The body is the user's own, so it is not +passed through `Notify.sanitize`; a remote image in a note will be fetched +when its preview renders. + +The Markdown body is shown verbatim when editing, so the page is not a WYSIWYG +editor: it is a plain note with a rendered view of it. + +## Files + + desktop/modules/notes/ + NotesModule.qml module, tile count, db.json FileView, writes and git + NotesPage.qml list and editor + Notes.js pure helpers, node-tested + test_notes.js the test + README.md + +`Notes.js` (`.pragma library`) holds the pure parts: parsing the whole +`db.json`, sorting newest first, formatting a timestamp, the next id, and the +transforms that return a new `db.json` object with a note added or removed +(every other key carried through untouched). The module calls them; the file +operations and git stay in QML. + +Registering: `import "modules/notes"` and a `NotesModule {}` in `shell.qml`'s +`modules` list. + +## Testing + +`node desktop/modules/notes/test_notes.js`: + +- Parsing a `db.json` written by `notes.sh`, `params` and `git` wrappers + included, keeps the whole object, not just `notes`. +- Sorting is by `file` descending, numeric, not lexicographic. +- The next id is the highest existing plus one, and `1` for an empty list. +- Adding then removing a note returns a db equal to the original, with + `params` and `git.lastpull` intact, and adding does not mutate the input + object. +- A malformed or truncated `db.json` yields an empty list rather than a + throw, so the page renders "no notes" instead of failing to load. + +The tests must be shown to fail on a broken input before they are trusted. + +## Not in scope + +Opening a note in `$EDITOR`. Restoring backups (`--restore`) or making one +(`--backup`). Markdown export. `--showconf` / `--userconf`. Pulling on open. +Watching `notes/` itself: a file deleted behind the page's back is noticed +when its body is next read, not before. |
