The native Markdown app for macOS.
Open ordinary Markdown files as polished Mac documents, edit their exact source, and preview them throughout Finder.
Free · MIT · macOS 14+ · no WebView
# Renderer handoff: the file stays readableThis is the working note for the renderer refactor. The page is a reviewsurface for a file people and coding agents change together: the source staysplain, the document stays legible, and every decision has a place to land.**Inline** · **bold** · _italic_ · ~~deprecated~~ · `RenderedDocument`**Links** · [open the issue](https://github.com/ezzy1630/Downright/issues) · [[Renderer]] · <https://downright.cc> · note[^1]**Math** · $e^{i\pi}+1=0$ · $\sqrt{x^2+y^2}$ · `$PATH`| Surface | State | Proof ||---|---|---|| `Parse` | **stable** | immutable block index || `Decorate` | **measured** | one pass per keystroke || `Review` | **visible** | word-level change marks |> [!NOTE]> Native: prose, source, state, and media in one surface.- [x] Parse once and decorate from ranges- [x] Keep raw bytes available for review- [ ] Move the image store off the main actor**Syntax** · comments, keywords, types, strings, numbers, and calls:```swift@MainActor func render(_ source: String) -> RenderedDocument { let index = parser.blockIndex(for: source) let image = cache.image(for: index) return decorator.apply(image, ranges: index.ranges)}```**Math**$$\mathop{\mathrm{parse}}(bytes) \longrightarrow \mathop{\mathrm{decorate}}(surface)$$**Mermaid**```mermaidflowchart LR A[MD] --> B{?} B -->|no| C[Keep] B -->|yes| D[Render]```## Review anchorsThe document map draws from this same structure, so every altitude has a floor to stand on.### SourceThe handoff stays source-first while the rendered surface stays native.### StateLinks, callouts, tasks, and math retain their local state as the page grows.### FinishThis note stays useful after the agent leaves because its structure remains visible.[^1]: Footnotes resolve locally without moving the reader away from the line.
Renderer handoff: the file stays readable
This is the working note for the renderer refactor. The page is a review
surface for a file people and coding agents change together: the source stays
plain, the document stays legible, and every decision has a place to land.
Inline · bold · italic · deprecated · RenderedDocument
Links · open the issue · [[Renderer]] · https://downright.cc · note1
Math · eiπ+1=0 · x2+y2 · $PATH
| Surface | State | Proof |
|---|---|---|
Parse | stable | immutable block index |
Decorate | measured | one pass per keystroke |
Review | visible | word-level change marks |
Parse once and decorate from ranges
Keep raw bytes available for review
Move the image store off the main actor
Syntax · comments, keywords, types, strings, numbers, and calls:
swift@MainActor func render(_ source: String) -> RenderedDocument {
let index = parser.blockIndex(for: source)
let image = cache.image(for: index)
return decorator.apply(image, ranges: index.ranges)
}
Math
Mermaid
Review anchors
The document map draws from this same structure, so every altitude has a floor to stand on.
Source
The handoff stays source-first while the rendered surface stays native.
State
Links, callouts, tasks, and math retain their local state as the page grows.
Finish
This note stays useful after the agent leaves because its structure remains visible.
This window is real. Type in it.
The difference
Press Space on a Markdown file today. This is what macOS shows you.
- MathReal symbols, typeset in the document.
- DiagramsMermaid, drawn as part of the document.
- TablesPipe rows become a real table.
- CalloutsGitHub alerts, drawn natively.
- FootnotesNumbered and linked, at the bottom.
- CodeSyntax colours from the app's own themes.
RAW MARKDOWN · QUICK LOOKThe same bytes above are live DOM — no uploaded image, no detour.
Markdown deserves to feel like a document.
Good typography, real selection, native shortcuts, and a source view when you need it. No vault or web view in the way.
Refactor notes — session 8f3c
Summary
Refactored the renderer pipeline to isolate decoration from parsing, replaced
the ad-hoc cache with a keyed image store, and updated 14 files. Behavior is
unchanged; 61 tests pass. Three public signatures moved; every call site is
updated in this changeset.
Files touched
Sources/MarkdownRender/Renderer.swift— split intoRenderer+DecoratorSources/MarkdownRender/ImageCache.swift— new, keyed by source hashSources/MarkdownCore/Parse.swift— block index now emits rangesTests/RendererTests.swift— +9 cases, snapshot fixtures updated
Decisions
1. Decoration is not parsing. The old renderer re-walked the parse tree
on every keystroke to apply marker elision. Parsing now produces an
immutable block index; decoration reads it. Keystroke cost drops from
three passes to one.
2. The cache belongs to the image, not the document. Documents swap;
images persist. Keyed by content hash so the same diagram rendered in
two documents shares one bitmap.
3. Ranges, not nodes. The block index emits byte ranges into the source
string. Anything that needs the tree can rebuild it; everything that
needs a location (selection, change marks, Quick Look) uses the range.
What I did not do
- Did not touch theme resolution. It works and it is load-bearing.
- Did not merge the two line scanners. They look similar; they are not.
One handles hard wraps, the other handles soft. Merging them cost 40ms
on a 10k-line file last time we tried.
- Did not add async parsing. The parse budget is 8ms and we spend 0.146ms.
Async would add complexity the numbers do not justify.
Signature changes
swift// before
func render(_ source: String, theme: Theme) -> NSAttributedString
// after
func render(_ source: String, theme: Theme) -> RenderedDocument
RenderedDocument carries the attributed string plus the block index, so
callers stop re-parsing to recover positions.
Test evidence
- 61 passed, 0 failed, 2 skipped (known GPU flake on CI, tracked in #412)
- Benchmark corpus: unchanged from yesterday's run
- Cold launch: 118ms → 121ms (+3ms, within noise, image store registration)
Open questions for review
1. Should Decorator own footnote numbering, or should the parse index?
Right now numbering survives decoration but not re-parse, which is
observable if you undo past a footnote insert.
2. The image store keeps 256MB of bitmaps. Under memory pressure we evict
LRU. Is 256MB the right ceiling for the M-series baseline?
3. ImageCache is @MainActor because NSImage is. If we ever move to
a background renderer this becomes the hard edge. Flagging now so it
is a decision, not an accident.
Next session
- Wire the change-mark dimming timer into
RenderedDocumentdwell state - Re-run the large-corpus benchmark on the MacBook Air (fanless) baseline
- Draft the migration note for the two downstream packages
See external edits without losing your place.
When another app or coding agent rewrites an open file, Downright marks what changed and protects any edits you have not saved.
Every change lands on a timeline, so you can compare any two versions later. Setup is three checkboxes and one default: nothing leaves your Mac.
Every number here has a limit beside it.
These are the app's own measurements. The corpus, the date, and the things we did not measure are all listed below the table.
| Measurement | p50 | p95 | Budget |
|---|---|---|---|
| Parse 100 KB | 12.078 ms | 12.395 ms | <250 ms |
| Source edit and paragraph map | 0.146 ms | 0.153 ms | <8 ms |
| Text diff, external rewrite | 2.099 ms | 2.164 ms | Informational |
| End-to-end semantic convergence | 30.583 ms | 31.445 ms | <100 ms |
| Syntax highlight, 10 KB Swift | 0.093 ms | 0.094 ms | Informational |
Your median parse this visit: waiting for a keystroke. Type in the living document and the measured result joins this table.
Corpus 120,825 characters / 4,885 lines. Date 2026-08-06. Machine single unrecorded machine.
Baseline only. Not a cross-machine claim. Missing: TextKit layout, scroll frames, IME input, live window frame time.
Your text stays in charge.
One window handles reading, editing, and source. Downright adds styling on top of your file. It never changes what is in it.
Read the repository- Your text is the only source of truth
- One window for reading, editing, and source
- No WebView anywhere
- Downright never changes a byte of your file
This page keeps its source too. Press ⌘⇧E.
It opens your files from anywhere.
Open a file from Finder. Preview it with Space. Flick a card and it slides — the cards have real weight. Or pipe a file through the down command.
Six themes. One document.
This page uses the app's theme engine. Pick a theme and the whole page changes with it.
Free. Open source. MIT. No account.
No app telemetry, no cookies, no account. The app stays local; this site uses anonymous, cookie-free analytics so we can see which pages help.
macOS 14.0+ · signed Downright.dmg · 1.0.16 · orbrew tap ezzy1630/downright && brew trust --cask ezzy1630/downright/downright && brew install --cask downrightBuilt by one person, funded by people, not telemetry. If Downright earns its place in your dock — sponsor it.
Downright lives on your Mac. Send it there.
No email, no server. On iPhone the first option in the share sheet is AirDrop.