bellows #
A local, encrypted notebook that grows as one line of replies. Every paragraph is at most 300 characters. You write the next one as a reply to any paragraph already on the line.
Originator: Nighthaven (@moja.blue)
The idea #
Niklas Luhmann kept his Zettelkasten as a box of numbered cards. Readers usually draw it as a map: ○→○→○. bellows rebuilds it from the writer's side: ○←○←○. Cards are not filed into categories. Each new card is written as a reply to an earlier one.
The data is one line of text. Paragraphs sit on the line in a fixed order. Nothing is reordered, and nothing is filed into folders.
- Every paragraph has an address. The address records the path to it: which paragraph of the main line it came from, and where on the branch it sits.
- Categories live outside the line. Hooks gather paragraphs on demand. The line itself never changes.
- The line folds and unfolds like a bellows. Press a hook and the paragraphs that do not match are hidden, so the line shrinks. Hidden paragraphs are not deleted, and the order stays the same. Press it again and the line opens back around the paragraph you were reading.
Addresses #
The rightmost group is the paragraph's number on the main line (four digits). Each group to the left is a step along a branch (three digits).
| Address | What it is |
|---|---|
0001 |
main line |
001 0001 |
branch from 0001 |
0002 |
main line |
001 0002 |
branch from 0002 |
002 0002 |
continues that branch |
001 002 0002 |
branch from 002 0002 |
0003 |
main line |
| Key | Action | Result |
|---|---|---|
| ⌘↵ | Continue | The next number on the same line. If that number is taken, a new branch starts instead. |
| ⇧⌘↵ | Branch | A new branch from the selected paragraph. |
With nothing selected, ⌘↵ adds to the end of the main line.
Clearing a paragraph keeps its number. You can write into that number again later.
To remove empty paragraphs, use Tidy empty paragraphs in the manage panel. It removes only empty paragraphs at the end of a line, so no number goes missing, and ⌘Z brings them back.
Write ←0003 or ←001 0002 in a paragraph to link to another paragraph.
Hooks #
Hooks sit in the bar at the top. There are two kinds.
| Kind | Mark | Gathers | How you make one |
|---|---|---|---|
| Word hook | hollow dot | every paragraph that contains the word | search, then pin the search |
| Branch hook | solid green dot | a main-line paragraph that starts with word:, and every branch under it |
write word: at the start of a main-line paragraph |
The contents button folds the line down to the first line of every main-line paragraph. Click a paragraph there to select it; double-click it to open the whole line around it. Search inside the contents to narrow it further, and pin that view as a hook too.
While a search is showing, a replace field appears above the line. One by one (Enter) walks through each match: Enter replaces it, ⇧Enter skips it, Esc stops. All (⌘Enter) replaces every match in the gathered paragraphs after a confirmation. ⌘Z undoes either.
Reading one paragraph at a time #
Select a paragraph and only that paragraph stays in full ink.
- Its number turns red.
- The paragraph it replies to has a blue number. Its replies have green numbers.
- Everything else fades.
- Typewriter mode keeps the selected paragraph's first line at 60% of the visible height.
| Key | Action |
|---|---|
| ↑ ↓ | Select the previous or next paragraph (inside a folded view, only the visible ones) |
| Enter | Edit the selected paragraph (inside a folded view, unfold the line around it) |
| double-click | Edit, with the cursor right after the character you clicked |
| ⌘↵ | Save an edit |
| Esc | Save the edit, then deselect, then unfold |
| ⌘Z / ⇧⌘Z | Undo or redo writing, editing, or clearing a paragraph. Undoing a new paragraph returns its text to the input box and leaves its number empty. |
| ⌘⌫ | Clear the selected paragraphs |
| N | Jump to the input box, keeping the selection, so what you write replies to it |
| Space | Preview the selected paragraph's images and PDFs; press again to close (← → to step through) |
| ⌘K | Search |
| ⌘-click or ⇧-click | Select several paragraphs. The eraser clears them all. |
In a folded view, clicking a paragraph selects it without unfolding, and ⌘-click and ⇧-click work as in the whole line. Enter or a double-click unfolds the line around the selected paragraph.
The buttons at the top right move you to the first paragraph, 10 main-line paragraphs back or forward, a random paragraph, and the last paragraph. Every move selects the paragraph it lands on and glides it to the typewriter position.
Writing format #
Text is set at 16px with 43 characters per line, so a full 300-character paragraph fills exactly seven lines. The manage panel offers three sizes (14, 16, 18px); the line length, line spacing, and number size keep the same proportions at each size, and the choice is stored, encrypted, in the box.
Paragraphs use Markdown: headings, bold, lists, quotes, code, tables, links.
Ruby (《》) and emphasis dots (《《》》) for Japanese are supported.
Attach up to three files to a paragraph. Images are re-encoded as WebP and stripped of EXIF.
While you edit a paragraph, its attachments appear below the text. Remove one with ×. Add one with the clip button, by pasting, or by dropping a file.
Click an image or a PDF to open it in the preview. The save button downloads an image as WebP and a PDF as it is.
Import and export #
Open the manage panel (the slider icon).
- Import a folder: every
.md,.markdown, and.txtfile under the folder is split into paragraphs at blank lines. Each file takes one main-line number, and its later paragraphs become a branch. The first paragraph records the source path, so a folder name works as a word hook. Files namedlog.mdand paths starting with.are skipped. Re-importing skips files that have not changed. - Import files: pick one or more
.md,.markdown, or.txtfiles instead of a folder. They are split and numbered the same way. A file whose content is already in the box is skipped, even under another name. - Export: whatever you are looking at (the whole line or a folded view) becomes one Markdown file. To restore a box, put an exported whole line (
bellows_YYYY-MM-DD.md) alone in a folder and import that folder into an empty box. Every address comes back.
Running #
Requires Python 3.9 or later and a Chromium-based browser. No extra packages are needed.
git clone https://tangled.org/moja.blue/bellows
cd bellows
python3 app/server.py
Open http://127.0.0.1:8782/.
On macOS, double-click start.command instead. It opens a Chrome incognito window. stop.command stops the server.
The first passphrase you enter creates a box. Write the recovery key on paper.
What it protects #
Anyone who copies the box files still cannot read them.
- Encryption and decryption happen only in the browser. The server stores and returns ciphertext and never sees the passphrase or the text.
- The passphrase goes through Argon2id (128 MiB, 4 passes) to wrap a random AES-256-GCM key.
- Every record is padded to a multiple of 1 KB. The length of what you wrote does not leak.
- Every record is bound to its position in the file. Records cannot be swapped or spliced without detection.
- The server listens on
127.0.0.1only. A strict Content Security Policy keeps the page from loading anything from outside. - Each attachment is its own encrypted file with a random name. An attachment that no paragraph uses any more is deleted when the box locks, or at the next open if the browser was closed first. Until then, ⌘Z can bring it back.
- The box locks itself after 10 minutes without input.
- Each passphrase opens its own box. A recovery key can reset the passphrase.
What it does not protect #
Read this before you trust it with anything important.
- Plain copies you make yourself: imported source files stay where they were, unencrypted. Exported Markdown is unencrypted.
- Old copies in backups: a cleared paragraph or removed attachment disappears from the box, but Time Machine, iCloud's Recently Deleted, or any other backup may still hold the old encrypted files. Anyone with the passphrase can open them. Overwriting before deletion does not help on SSDs and APFS.
- When and how often you wrote: records are appended one by one. Anyone with the files sees the number of writes and their times.
- A compromised computer: while a box is open, the text sits decrypted in browser memory. A keylogger captures the passphrase.
- Independent review: the primitives are standard (WebCrypto AES-GCM, Argon2id). The way they are combined here has not been audited.
- Proof of priority: timestamps come from your own clock. They are not evidence of when you had an idea.
- A lost passphrase and a lost recovery key: nobody can open the box.
Files #
matters/<box>/box.head salt and the wrapped key (plain JSON)
matters/<box>/box.log encrypted records
matters/<box>/att/<name> one encrypted attachment per file (random 12-hex name)
piles/<box>/ the previous log and head versions
matters/ and piles/ are in .gitignore. Never commit them.
Language #
The user interface is in Japanese.
License #
MIT. See LICENSE and THIRD_PARTY.md.
- HTML 89%
- Python 10%
- Shell <1%