Point Orrery at a folder. Your notes stay ordinary markdown on disk, joined by
[[wikilinks]]. That folder usually holds code as well, so Orrery edits code
properly, shows git diffs, and gives you a shell in the same window.
Note apps stop at notes. Code editors treat links between documents as text. In practice a folder of notes and a folder of code are the same folder.

Writing
Formatting renders as you type. Headings size up, bold is bold, links show their label. The syntax markers come back the moment your cursor lands on them. Nothing is converted to a rich-text model and back, so the file on disk is what you typed.
Three modes on Ctrl+Shift+1 through 3: edit, hybrid, reading.

Tables tidy themselves. Mermaid fences become diagrams. $maths$ goes through
KaTeX. A long code fence scrolls inside its own card instead of pushing the
document around.
The whole folder as a map

The graph draws the vault. Size nodes by links, influence or bridging. Colour them by cluster or folder. A note you linked to but never wrote appears as a ghost, so the gaps show up next to the structure.
Code is on the map too. Notes point at each other with wikilinks, source files
point at each other with imports. Both are drawn, so a note that links
[[pane]] reaches pane.ts. Imports resolve only to files inside the vault: a
dependency on react is real, but it is not part of this folder.
Code, git and a terminal

Git is built in. Stage, unstage, discard, commit, change bars in the gutter. A diff opens as a tab holding two real editors with syntax highlighting, undo and selection. The working-tree side saves with Ctrl+S.
The commit graph draws every branch. Click a commit for the files it touched, then a file for that commit's diff against its parent.

Language servers are used where you already have them: hover types, completion, diagnostics, go-to-definition. Orrery bundles none of them.
Files that are not text



A PDF opens as a document: pages, selectable text, find, an outline, thumbnails.
Vault search reads inside them. [[paper.pdf#page=12]] opens at that page. You
can also change one: highlight, draw, fill a form, rearrange pages, or retype a
line in the document's own font. Nothing is written until you save.
A .csv opens as a grid you can sort and filter without touching the file. A
.db opens as its tables, read-only twice over: the file is opened read-only,
and anything that is not a question is refused before it reaches the database.
Reading a web page you did not write

An HTML file opens as source. The Read switch renders it, and this is the one place Orrery displays untrusted markup as markup, so the terms are strict.
The page runs in a sandboxed frame under a policy that starts by refusing
everything. Its stylesheet and pictures load from one folder, over a scheme that
resolves paths in the main process, so a page asking for ~/.ssh/id_rsa gets
nothing. Nothing loads from the internet until you ask. The page's own code does
not run until you ask. Code fetched from the internet never runs, at any
setting: loading a picture from someone's server tells them you opened their
file, and running their code hands them the page you are reading.
Two things it draws that a browser would need the page's scripts for: mermaid diagrams and TeX. Both rendered by Orrery from the page's own source, before anything has run.
Built to be argued with
Layers do not cross. core/ is pure logic that runs in a bare Node process with
no Electron, no DOM and no editor, which is why its tests need no build step.
main and renderer never import each other; they meet at one file that
declares every IPC channel, validated with zod at the boundary. A test enforces
all of it, so a layer violation turns a test red rather than becoming a habit.
Document text lives in CodeMirror state rather than in React, so no large string travels through a render on a keystroke. Saves go through a temporary file and check the mtime first, so a write cannot silently discard a change made outside the editor.
Because it is counted as coverage. Several bugs here were found by breaking a
fix and watching the test stay green. One case: a whole file of "it ignores X"
assertions passed because jsdom delivers postMessage with event.source set
to null, so the code rejected everything, including the case that was supposed
to work. The test that caught it was the one asserting the positive.
Where the time actually went
Every performance bug in this project had an obvious cause that turned out to be wrong. Measuring first was worth more than any optimisation.
▶Three that were measured before they were fixed
Opening a huge folder. Suspected the graph and analytics. It was a serial directory walk producing 70 MB of JSON, and a recursive watcher that had not finished after sixty seconds.
Start-up with many tabs open. Suspected serial file reads and store churn. The reads were 75 ms of 4,564 ms and the store updates were 6 ms. The cause was one line setting the active tab on every pass of a loop, so the visible pane displayed every restored document in turn before settling on the one you wanted.
Start-up freezing for fifteen seconds. Suspected the file walk. The walk was
493 ms. resolveImport was rebuilding a set of every path in the vault, per
import, which on a 3,700-file repository is hundreds of millions of string
operations. Indexed once: 15,155 ms to 1,775 ms.
| Stack | Electron 43 · React 19 · TypeScript strict · CodeMirror 6 · Zustand |
| Layers | core → shared → main / preload / renderer, enforced by a test |
| Renderer | context isolation, sandbox, webSecurity on, no Node |
| Untrusted HTML | own scheme, own policy per document, disk access confined to a folder |
| Search | TypeScript, with an optional Rust sidecar: 34 ms against 543 ms |
| Platforms | Linux (.deb, AppImage). macOS and Windows targets exist, unbuilt |
| Licence | MIT |
Get it
Orrery on GitHub
Source, MIT
Linux packages
Debian and AppImage builds, on GitHub Releases