Skip to content

Releases: lazypower/VoidReader

VoidReader 1.2.0 — "Reading the Fine Print"

26 Apr 16:00

Choose a tag to compare

VoidReader 1.2.0 — "Reading the Fine Print"

The one where we learn what your document is about before you do

You know how every markdown file starts with a block of YAML that nobody reads? Turns out it's actually useful — title, author, tags, dates, all that metadata people carefully maintain and then scroll past. VoidReader now reads it for you and puts it somewhere nice.

Frontmatter Banner

Documents with YAML frontmatter (--- fenced at the top) now render a styled banner above the content. Key-value pairs display as labeled rows, and values that look like comma-separated lists (looking at you, tags:) wrap into tidy pills. It's the kind of thing you didn't know you wanted until you see it, and then you wonder why every markdown viewer doesn't do this.

Works in the reader, in print output, and in Quick Look previews — because metadata doesn't stop being metadata just because you hit ⌘P.

Scroll Percentage (Again)

Yes, again. Turns out 1.1's progressive rendering — the thing that made large documents load 134x faster — had a side effect: the scroll percentage was calculated against the initial chunk's height, not the full document. So you'd open a 50,000-row table, see "100%" immediately, and wonder if the document was really that short. It was not.

The fix is one line: recalculate scroll percentage when the denominator changes after progressive render completes. The regression test suite is considerably more than one line, because we're tired of fixing this.

Regression Tests

Four unit tests proving the progressive-render math stays honest, plus two UI tests that open pathological fixtures and verify the percentage starts at zero. CI generates the fixtures on the fly and cleans up after itself, because checking 50,000-row tables into git felt wrong.

Housekeeping

Centralized the code block chrome constants (header height, padding) into one place instead of three. Copilot noticed the drift risk before we did — credit where it's due.

Performance Lab

All four scenarios green, no regressions from frontmatter work:

Scenario Samples Idle Work Top App Frame
open-large 458 12 (2.6%) 433 (94.5%) VoidReaderApp.$main() 91%
scroll-to-bottom 768 14 (1.8%) 667 (86.8%) Low — no single hotspot
search-navigate 503 14 (2.8%) 471 (93.6%) MarkdownChunker.findFirstChunkEnd 0.6%
edit-toggle 2,248 1 (0.04%) 1,842 (81.9%) AttributedString.init 10.5%

The edit-toggle AttributedString dominance is known and pre-existing — it's CoreText doing legitimate work building styled text for the editor. Nothing new, nothing alarming.

Install / Update

brew install --cask lazypower/tap/voidreader
# or
brew upgrade --cask voidreader

Or grab the DMG from Releases.

Full Changelog: v1.1.0...v1.2.0

Co-authored with Claude Opus 4.6, who has now fixed the scroll percentage in three consecutive releases and is starting to take it personally.

v1.1.0 — We Measure Twice Now

22 Apr 04:17

Choose a tag to compare

VoidReader 1.1 — "We Measure Twice Now"

The "we built a ruler before we cut" release

So 1.0 shipped. People loved it. Then someone opened a 50,000-line markdown file and went to make coffee while VoidReader contemplated the meaning of life for about 90 seconds. That was... not great.

The first instinct was to optimize — start swinging at whatever looked slow and hope the profile agreed. That instinct is how codebases end up with three caching layers, a LazyVStack wrapped in a ScrollView wrapped in regret, and nobody able to tell you why anything is faster.

So we did the other thing. We instrumented first. Shipped OSSignposter across every subsystem, built a performance lab that runs traces through xctrace in CI, and let the profile tell us what to fix. Then we fixed those things, in order, one at a time, with receipts.

This release is the result.


The Headlines

134x faster load time for large documents — 1,475ms → 11ms for a 60k-line doc. Not a typo. The trick: show you the first page while we render the rest in the background. Civilized software, arguably.

Large code blocks don't lock up the UI anymore — a 100,000-character code block used to stall the main thread while Highlightr tokenized. Now measurement runs off-main on a serial queue, and the block gets segmented across virtualized rows so SwiftUI only renders what's on screen.

Search doesn't walk the document for every keystroke — TextSearcher used to do an O(M·N) line-count on every match, and the find-bar re-searched the whole doc to populate result previews. Both fixed. Search on a 60k-line doc now feels local, not "please stand by."

Tables bigger than your monitor render smoothlyTableBlockView pre-measures rows and virtualizes, so a 50,000-row CSV table scrolls like a native grid instead of a powerpoint slide loading over dial-up.

Instruments-grade observability — OSSignposter on document lifecycle, rendering passes, image/mermaid/scroll subsystems. Open Instruments, hit record, read the timeline. No more guessing.


What Actually Happened

We Built a Ruler (add-performance-lab)

The whole "measure before you cut" story is backed by an OpenSpec change called add-performance-lab — a discipline layer, not a feature. It defines:

  • A set of fixture documents at known pathological sizes (100k code block, 250k mixed, 50k table)
  • A CI job that runs xctrace against each fixture, collecting timing data
  • A gate that diffs measurement windows against historical baselines

Also: turns out xctrace lies about whether a trace recorded. Its exit code can be non-zero on completed traces, and zero on failed ones. The CI was false-failing on successful runs and false-passing on broken ones. We narrowed the success gate to grep the trace log for the actual failure string — Recording failed with errors — rather than trusting the exit code. A whole category of "why did CI go green" mysteries dissolved.

Performance

  • Progressive rendering: AST-aware chunker slices the doc at block boundaries, first ~20KB renders immediately, rest loads in background
  • Off-main measurement cache: code block height + highlighted attributed strings computed on a serial queue (Highlightr's JSContext is thread-pinned, which shaped the architecture more than we expected) and cached per-document
  • Document-wide height index: scroll math uses a pre-computed cumulative height table instead of asking each block at scroll time
  • Large-code-block segmentation: 100k-character blocks split across LazyVStack rows with a shared groupID, so SwiftUI virtualizes rows of a single logical block. Counter-intuitive — SwiftUI renders N small things much better than one big thing.
  • Visible-region editor highlighting: syntax highlighting runs on what's on screen plus a buffer, not your entire 2MB manifesto
  • LazyVStack virtualization for the reader view
  • Chunked rendering: documents >1000 blocks render in digestible chunks
  • Cached regex: InlineMathParser no longer recompiles its regex on every. single. call.
  • Debounced scroll tracking: 200ms idle before updating position (your CPU says thank you)

Search

Three commits to teach the find-bar that searching a big document shouldn't require walking it three times per keystroke:

  • Cached match texts so the body-evaluation path doesn't re-search the full doc
  • Dropped an O(M·N) line-count + index round-trip in TextSearcher
  • Cached the highlighted AttributedString at the search-key boundary, not per-render

Net: typing in the find-bar on a 60k-line document no longer feels like dial-up.

Observability

  • OSSignposter infrastructure: Signposts.lifecycle, Signposts.rendering, plus subsystem posts for Mermaid render, image load, scroll tick
  • Debug telemetry: set VOID_READER_DEBUG=1 to see timing data for every subsystem
  • File logging: VOID_READER_DEBUG_FILE=/tmp/vr.log for post-mortem analysis
  • make profile: one command to launch with signposts wired for Instruments
  • make run-debug: one command to run with telemetry enabled

Sagas Worth Telling

The scroll percentage tracking saga. Three commits to get this right. Turns out GeometryReader must be outside LazyVStack to fire continuously during scroll. LazyVStack is lazy — it doesn't re-render during scroll, so preference keys inside it only fire once. Big thanks to Fiona for the architectural clarity on that one.

The segment-rows pivot. Originally tried to route large code blocks through a single NSTextView wholesale. It worked up to about 30k characters, then TextKit started thinking. A lot. The pivot was to stop thinking of a big code block as one thing — split it across LazyVStack rows with a shared groupID so SwiftUI virtualizes rows of a single logical block. Rendering a 100k-character block is now meaningfully faster than rendering a 10k-character block was in 1.0, because most of the rows never render at all.

The xctrace masquerade. Covered above under the perf lab, but it deserves its own beat: the first version of the CI gate trusted xctrace's exit code. It shouldn't have. Narrowing the success signal to the trace log's actual error string turned a whole class of flaky failures into deterministic ones. "Don't trust the exit code of a tool that processes its own crashes" is a hard-won lesson.

Developer Experience

  • XCUITest infrastructure: 7 automated tests because we're professionals now
  • Gitea CI: full .gitea/workflows/ for perf lab + UI tests, running on self-hosted runners
  • AST-aware chunker (also a perf thing, but makes future block-level features much easier to build)

Technical Details for the Curious

Metric 1.0 1.1
Time to first content (60k lines) ~1,475ms ~11ms
Editor rehighlight scope Full document Visible region + buffer
100k-char code block Main-thread stall Off-main, virtualized
Find-bar keystroke on large doc Full re-search Cached match texts
Memory (60k lines) Untested ~370MB
Scroll tracking Disabled for large docs Works everywhere
Observability print() OSSignposter + perf lab

Up Next

  • Memory optimization (370MB is acceptable but there's fat to cut)
  • Block height refinement for smoother scroll position estimates
  • Inline math ($x^2$) support
  • More polish for the markdown linter

Full Changelog: v1.0.4...v1.1.0

Co-authored with Claude Opus 4.5 and 4.7, who between them have learned that performance testing should happen before shipping, exit codes lie, and the right response to a slow system is a ruler, not a hammer.

VoidReader 1.0.4

04 Mar 18:54

Choose a tag to compare

VoidReader 1.0.4 — "Math Is Hard"

The one where we learn that not all blocks are 60 pixels tall

Turns out, estimating total document height as blockCount × 60px works great — right up until your document contains Mermaid diagrams, KaTeX equations, code blocks, and tables that are decidedly not 60 pixels tall. The scroll percentage was confidently reporting 100% when you'd barely made it past the introduction. Bold of it.

Scroll Percentage Uses Real Measurements Now
Swapped the vibes-based height estimation for actual measured dimensions from GeometryReader. The percentage now tracks reality, which is apparently a feature people expect from a progress indicator. Extracted ScrollPercentage.calculate() into VoidReaderCore so we could write 9 unit tests proving we can do arithmetic, plus 4 UI tests that open a mixed-content document and verify the number goes up when you scroll down. Groundbreaking.

Install / Update

brew install --cask lazypower/tap/voidreader
# or
brew upgrade --cask voidreader

Or grab the DMG from Releases.

Full Changelog: v1.0.3...v1.0.4

Co-authored with Claude Opus 4.6, who now understands that 60px is not a universal constant.

VoidReader 1.0.3

27 Feb 03:01

Choose a tag to compare

VoidReader 1.0.3 — "Scroll Like You Mean It"

The one where diagrams stop cosplaying as thumbnails

Mermaid Diagrams Actually Fill The Page Now
Mermaid was generating SVGs at their "natural" size, which apparently means "sized for ants." Diagrams now scale to fill the reading width using proper CSS vector scaling. Lossless, responsive, legible. Sequence diagrams are no longer a suggestion.

Scroll Performance: Tables Were The Villain
Tables were built from 170+ individual sticky notes (nested VStack/HStack/ForEach). SwiftUI was doing N² layout negotiations every time a table scrolled into view. Switched to Grid — the layout primitive that was right there the whole time. One line change. Massive improvement. We don't talk about it.

Code Blocks Stop Re-Highlighting Every Frame
Syntax highlighting was running on every single view body evaluation. During a 60fps scroll, that's 60 full Highlightr passes per second, per visible code block. Now it highlights once and caches the result. Your CPU sends its regards.

Broken Mermaid Diagrams Stay Broken (Gracefully)
Failed diagrams used to spin up a fresh WKWebView, load mermaid.js, parse, fail, and repeat — every time you scrolled past. Now the first failure gets cached and subsequent encounters skip straight to the code fallback. Click the new info icon to see what mermaid is complaining about.

Automated Release Pipeline
No more hand-carrying DMGs like it's 2004. Push a tag, GHA builds, signs, notarizes with Apple, creates the release, uploads the DMG, and updates the Homebrew cask. The future is now Jiffin.

Install / Update

brew install --cask lazypower/tap/voidreader
# or
brew upgrade --cask voidreader

Or grab the DMG from Releases.

Full Changelog: v1.0.1...v1.0.3

VoidReader 1.0.1 — "We Measure Twice Now"

20 Feb 03:54

Choose a tag to compare

The "whoops, maybe we should test with big files" release

So 1.0 shipped. People loved it. Then someone opened a 50,000-line markdown file and went to make coffee while VoidReader contemplated the meaning of life for about 90 seconds. Our bad.

This release fixes that. Aggressively.


The Headlines

134x faster load time for large documents — What used to take 1,475ms now shows content in 11ms. That's not a typo. The trick? Show you the first page while we render the rest in the background like civilized software.

Smooth scrolling on massive files — LazyVStack virtualization means we only render what you can see. Novel concept, apparently.

Read percentage actually works now — It was disabled during the panic optimization session. Now it tracks your scroll position continuously, even on documents large enough to be their own ZIP code.


What We Fixed (The Full Confession)

Performance

  • Progressive rendering: First ~20KB renders immediately, rest loads in background
  • Visible-region editor highlighting: Only syntax highlight what's on screen + a buffer, not your entire 2MB manifesto
  • LazyVStack virtualization: Virtual scrolling for the reader view
  • Chunked rendering: Documents >1000 blocks render in digestible chunks
  • Cached regex: InlineMathParser no longer recompiles its regex on every. single. call.
  • Debounced scroll tracking: 200ms idle before updating position (your CPU says thank you)

Scroll Percentage Tracking (The Saga)

Three commits to get this right. Turns out GeometryReader must be outside LazyVStack to fire continuously during scroll. LazyVStack is lazy — it doesn't re-render during scroll, so preference keys inside it only fire once. Big thanks to Fiona for the architectural clarity.

Developer Experience

  • Debug telemetry: Set VOID_READER_DEBUG=1 to see timing data for every subsystem
  • File logging: VOID_READER_DEBUG_FILE=/tmp/vr.log for post-mortem analysis
  • XCUITest infrastructure: 7 automated tests because we're professionals now
  • make run-debug: One command to run with telemetry enabled

Commits Since 1.0

fix: scroll percentage updates continuously during scroll
fix: wire up scroll percentage tracking correctly
fix: re-enable scroll percentage for large documents
perf: LazyVStack virtualization and chunked rendering
perf: progressive rendering and visible-region editor highlighting
refactor: minor syntax highlighter and stats optimizations
feat: add debug telemetry and XCUITest infrastructure
chore: add perf optimization spec and test doc generator

Technical Details for the Curious

Metric 1.0 1.1
Time to first content (60K lines) ~1,475ms ~11ms
Editor rehighlight (visible only) Full document ~15K chars
Memory (60K lines) Untested ~370MB
Scroll tracking Disabled for large docs Works everywhere

Up Next

  • Memory optimization (370MB is acceptable but we can do better)
  • Block height refinement for smoother scroll position estimates
  • Inline math ($x^2$) support
  • More polish for the markdown linter

Full Changelog: v1.0.0...v1.1.0

Co-authored with Claude Opus 4.5, who has learned that performance testing should happen before shipping.

v1.0.0 — The "We Actually Shipped" Release

17 Feb 18:05

Choose a tag to compare

   \ \    / /  (_)   | |  __ \              | |
    \ \  / /__  _  __| | |__) |___  __ _  __| | ___ _ __
     \ \/ / _ \| |/ _` |  _  // _ \/ _` |/ _` |/ _ \ '__|
      \  / (_) | | (_| | | \ \  __/ (_| | (_| |  __/ |
       \/ \___/|_|\__,_|_|  \_\___|\__,_|\__,_|\___|_|

                      v1.0.0

Shipped.

From hello world to here:

  • Native markdown rendering (no web views for text)
  • Mermaid diagrams, LaTeX math, image support
  • Split-pane editing with live preview
  • Theming with Catppuccin
  • Linting & formatting
  • Print/PDF with proper pagination
  • Quick Look extension
  • That gorgeous document icon

Zero Electron. All craft.

Cheers to Chuck and Fiona. The void is beautiful.

v1.0.0 — The "We Actually Shipped" Release

VoidReader is complete.

What started as "I just want to read a markdown file without suffering" is now a fully-realized native macOS app. Every feature polished. Every pixel considered.

What's New Since 0.3.0

The Crown Jewel

  • Custom document icon — galaxy-themed beauty for your .md files (designed by Fiona)

Reader Theming

  • New "Apply Theme to Reader" toggle — keep reader native macOS, or go full Catppuccin
  • Theme colors now flow to headings, list markers, blockquotes

Math Gets Better

  • Inline math ($...$) now renders as styled text inline with your paragraphs

Print & PDF Finally Done Right

  • Mermaid diagrams render as images (no more placeholders)
  • Document images included in output
  • Proper pagination — blocks won't get guillotined at page boundaries

The Numbers

Capabilities shipped: 9
Lines of Swift: ~12,000
Electron dependencies: 0
Web views for text: 0
Time to first render: Instant

What You Get

A markdown viewer that opens files and shows them to you. Native rendering. Mermaid diagrams. LaTeX math. Syntax highlighting. Linting. Theming. Print to PDF.

No vaults. No plugins. No subscription. No Electron.

Just your documents, rendered beautifully.


VoidReader: Because sometimes you just want to read the damn file.

VoidReader 0.3.0 - The Judgmental Release

17 Feb 05:44

Choose a tag to compare

VoidReader now has opinions about your markdown. Strong ones.

What's New

Markdown Linter

8 rules to keep your docs respectable:

Rule What It Judges
MD001 Headings should increment by one (no skipping leg day)
MD004 Pick a list marker and commit to it
MD009 Trailing whitespace is not a personality trait
MD012 One blank line is enough, we get it
MD022 Headings need breathing room
MD026 Headings aren't sentences, drop the punctuation
MD031 Code blocks deserve personal space too
MD049 Emphasis markers should be consistent

Warning count badge in the status bar. Know your shame at a glance.

Markdown Formatter

Auto-fix for the chaos:

  • Format Document (Cmd+Shift+I) - On-demand tidying
  • Format on Save - Toggle in Settings for the disciplined
  • Normalizes list markers, emphasis, whitespace
  • Aligns table columns like a civilized editor
  • Adds blank lines where they belong

All rules auto-fix except MD001 (we can't read your mind about heading levels).

Bug Fixes

  • Font size slider now updates reader view (not just code blocks)
  • No more false "document modified" prompts
  • Smoother edit mode transitions
  • Better performance on large documents

Install

Download VoidReader.dmg, mount, drag to Applications. Notarized and signed.

Or build from source:

make project && make build

A markdown viewer for people who actually read markdown. No Electron. No web views for text. Just your documents, rendered beautifully.

The "I just wanted to read markdown" release.

16 Feb 08:48

Choose a tag to compare

VoidReader 0.1.0

Features

  • Native markdown rendering with AttributedString (no web views for text)
  • Mermaid diagram support (flowcharts, sequence diagrams, etc.)
  • Syntax-highlighted code blocks with one-click copy
  • Full GFM support: tables, task lists, strikethrough
  • Outline sidebar for document navigation
  • Edit mode with AST-based syntax highlighting (Cmd+E)
  • Distraction-free mode (Cmd+Shift+D)
  • Theming: System (native macOS colors) + Catppuccin (Mocha/Latte)
  • Custom themes via JSON in ~/Library/Application Support/VoidReader/themes/
  • Quick Look extension for Finder previews
  • External file change detection

Distribution

  • Signed and notarized for macOS
  • Universal binary (Intel + Apple Silicon)