The native Markdown app for macOS.

Open ordinary Markdown files as polished Mac documents, edit their exact source, and preview them throughout Finder.

Download for macOS

Free · MIT · macOS 14+ · no WebView

sample.md
# 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

SurfaceStateProof
Parsestableimmutable block index
Decoratemeasuredone pass per keystroke
Reviewvisibleword-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

parse(bytes) decorate(surface)

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.

Line 1, Column 1Markdown— wpm· type to measureparse —· on first keystrokeframe floor 16 ms

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 into Renderer + Decorator
  • Sources/MarkdownRender/ImageCache.swift — new, keyed by source hash
  • Sources/MarkdownCore/Parse.swift — block index now emits ranges
  • Tests/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 RenderedDocument dwell 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.

External write

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.

Current local benchmark · every row has its limit beside it
Measurementp50p95Budget
Parse 100 KB12.078 ms12.395 ms<250 ms
Source edit and paragraph map0.146 ms0.153 ms<8 ms
Text diff, external rewrite2.099 ms2.164 msInformational
End-to-end semantic convergence30.583 ms31.445 ms<100 ms
Syntax highlight, 10 KB Swift0.093 ms0.094 msInformational

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
One surface, two honest faces

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.

Markdown4 files · flick one, press Space to Quick Look
down — the document from the terminalzsh

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.

Download for macOSmacOS 14.0+ · signed Downright.dmg · 1.0.16 · or brew tap ezzy1630/downright && brew trust --cask ezzy1630/downright/downright && brew install --cask downright

Built 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.

Keyboard shortcuts
⌘KCommand palette
⌘⇧EFlip the window to its source
SpaceQuick Look the focused file card
1–5Structural zoom level
← → ↑ ↓Divider, zoom, rail, palette
EscClose whatever opened