Skip to content

Add Quick Look previews, font settings, and a native installer - #15

Open
santiagobarros wants to merge 11 commits into
JackYoung27:mainfrom
santiagobarros:pr/quicklook-preview
Open

Add Quick Look previews, font settings, and a native installer#15
santiagobarros wants to merge 11 commits into
JackYoung27:mainfrom
santiagobarros:pr/quicklook-preview

Conversation

@santiagobarros

Copy link
Copy Markdown

What this adds

Quick Look (spacebar) previews in Finder with full component parity: tables, task lists, code blocks, images, LaTeX math, and Mermaid diagrams — plus a document font setting, a native .pkg installer, and distribution infrastructure (signing/notarization when certificates are available).

Quick Look architecture

  • A data-based QLPreviewProvider app extension (src/quicklook.m), built into Contents/PlugIns by build.sh — no Xcode required, same script-driven build as the rest of the repo.
  • Markdown + math render under JavaScriptCore: marked runs in a JSContext; math ($…$, $$…$$, \(…\), \[…\]) is extracted from the source (code-fence aware) and rendered with katex.renderToString, which needs no DOM. KaTeX CSS ships with its woff2 fonts inlined as data URIs.
  • Images become cid: attachments on the QLPreviewReply (data-based previews have no base URL), with a read-only filesystem exception so previews can load images referenced from anywhere. macOS additionally shows its one-time privacy consent for Desktop/Documents/Downloads.
  • Mermaid cannot run in the extension — empirically, the Quick Look sandbox on macOS 26 terminates WebKit's WebContent process immediately, and view-based (preparePreviewOfFileAtURL:) extensions are never launched by quicklookd at all (both approaches were implemented and reverted; findings documented). Instead: the app caches every SVG it renders (keyed by sha256 of the diagram source, both light and dark themes), the extension reads that cache, and an optional on-demand launchd helper (./install.sh --with-mermaid-helper, off by default because it registers a Login Items entry) renders never-before-seen diagrams live. Without the helper, unseen diagrams degrade to a styled source block.

Also included

  • Settings window (⌘,) with a document font preference: the current serif, GitHub's system-sans stack, or Geist (vendored from the geist npm package with a pinned SHA-256, like the other libraries).
  • ./build.sh installer — a signed-when-possible .pkg whose customize pane offers "default .md viewer" and the Mermaid helper as checkboxes.
  • Trust/permissions cleanup: the launch-time GitHub update check is gone (the README promises no network calls) and replaced by a manual "Check for Updates…" menu item; the extension is sandboxed read-only; the helper validates XPC callers by team identifier when real signing is present.
  • Signing pipeline: build.sh auto-detects Developer ID → Apple Development → ad-hoc (unchanged default behavior for contributors without certificates), plus ./build.sh notarize.
  • Bug fix: installs now replace the existing bundle instead of merging over it, which broke the code-signature seal.

Verification

Unit-tested render pipeline; end-to-end verified through real QLPreviewPanel sessions (note: qlmanage -p crashes on macOS 26 for all third-party QL extensions, including QLMarkdown — panel-based testing is the workaround); notarization accepted by Apple for both the app and installer on a Developer ID account.

Happy to split this into smaller PRs (e.g. update-check fix, fonts, Quick Look, installer) if you prefer reviewing piecewise.

A stale copy merged over by ditto keeps files the new bundle no longer
ships, which breaks the code-signature seal and makes Gatekeeper reject
the app.
The README promises no network calls, but the app contacted the GitHub
releases API on every launch. The check now runs only from a new
"Check for Updates…" menu item, with explicit feedback for up-to-date
and unreachable cases.
New Settings window (Cmd+,) with three document fonts: the existing
serif, GitHub's system sans stack, and Geist (vendored from the geist
npm package with a pinned SHA-256 like the other libraries). The choice
persists in NSUserDefaults and is applied live to all open windows as a
data-font attribute driven by an injected user script.
A data-based QLPreviewProvider app extension built into
Contents/PlugIns by build.sh — no Xcode project required. Markdown
renders with the bundled marked.umd.js under JavaScriptCore; LaTeX math
is extracted from the source (code-fence aware, so $a_i$ survives
emphasis parsing) and rendered with katex.renderToString, which needs
no DOM — Quick Look executes no JavaScript in data-based previews.
KaTeX CSS ships with its woff2 fonts inlined as data URIs. Local images
referenced by the markdown are embedded as cid: attachments on the
QLPreviewReply, since data-based previews have no base URL; a read-only
filesystem exception lets the sandboxed extension read them (macOS
additionally asks once for privacy-protected folders like Desktop).
Mermaid fences degrade to a styled source block for now.

The appex is signed with its entitlements before the outer app —
codesign --deep would strip them. Note for testing: qlmanage -p crashes
on macOS 26 for all third-party Quick Look extensions; use a
QLPreviewPanel harness or Finder itself.
The Quick Look sandbox cannot host a browser engine (WebContent
processes are terminated immediately, and view-based preview extensions
are never launched by quicklookd — both verified on macOS 26), so
Mermaid is solved in two layers:

1. The app caches every SVG it renders, keyed by sha256 of the trimmed
   diagram source, in both light and dark themes (viewer.js renders the
   non-displayed theme in the background after the visible pass).
2. On cache miss the extension asks an optional on-demand launchd
   helper over XPC. launchd registers no running process at rest,
   spawns the helper when the extension connects, and it exits after
   45 seconds idle. It is opt-in (./install.sh --with-mermaid-helper)
   because launchd agents surface as Login Items.

The extension prefers the SVG matching the current system appearance,
then a live helper render, then a stale-theme SVG; unseen diagrams with
no helper degrade to the styled source fallback. A permissions section
in the README documents the model.
./build.sh installer builds a standard macOS installer whose customize
pane offers two optional selections: setting Markdown Viewer as the
default .md handler (pre-selected) and installing the Mermaid Quick
Look helper (off by default, since it registers a Login Items entry).
The choices run per-user postinstall scripts that call the same
set-default-handler.py and register-mermaid-helper.sh now shipped in
the app's Resources; install.sh reuses them instead of embedding its
own copies. The app package removes any previous install before laying
down the payload so stale files cannot break the signature seal.
build.sh auto-selects a signing identity — CODESIGN_IDENTITY override,
then Developer ID Application (with hardened runtime and secure
timestamp, the notarization prerequisites), then Apple Development,
then ad-hoc (unchanged default for contributors without certificates) —
and reports what it used. New ./build.sh notarize submits the release
zip via notarytool and staples the ticket; the installer is signed when
a Developer ID Installer certificate exists.

The render helper derives its own team identifier from its signature
and rejects XPC callers signed by any other team (macOS 13+
setCodeSigningRequirement). Ad-hoc builds carry no team, so local
development skips the check.
@santiagobarros

Copy link
Copy Markdown
Author

Restructured the branch into feature-scoped commits so you can cherry-pick exactly what you want — each commit builds and works on its own:

  1. Replace the existing bundle on install instead of merging over it — standalone bugfix
  2. Make the update check user-initiated — aligns the app with the README's no-network-calls promise
  3. Add a document font setting: Serif, GitHub, or Geist
  4. Add a Quick Look extension: spacebar previews in Finder — markdown, math, images (Mermaid degrades to a styled source block at this point)
  5. Render Mermaid in Quick Look and follow the system theme — the cache + optional helper; depends on 4
  6. Add a .pkg installer with optional-feature choices — depends on 5 for the helper checkbox
  7. Add signing, notarization, and XPC caller validation — no-ops safely to ad-hoc signing without certificates

1–4 are each independent of everything after them; the natural cherry-pick prefixes are 1, 1–2, 1–3, 1–4, etc.

On the first launch where another app (usually Xcode on a developer
Mac) owns Markdown files, ask once whether to make Markdown Viewer the
default, using NSWorkspace's modern default-application API. Declining
is remembered; if the app is already the default (e.g. the installer
checkbox or install.sh set it), the prompt never appears.
…sions

New optional, pre-selected choice: elects Markdown Viewer's Quick Look
extension over any other Markdown previewer already registered (e.g.
QLMarkdown), via pluginkit -e use, mirroring the existing default .md
handler choice. install.sh now does the same unconditionally, matching
its existing unconditional default-handler behavior.
…ages

Installer.app renders custom welcome/conclusion resources via the
legacy WebKit1 WebView class, which is deprecated (since macOS 10.14)
and crashes on current macOS when that code path is exercised — this
is what actually caused 'the installer says it failed' even though the
payload had already been applied. RTF resources are rendered by
TextKit directly, with no WebKit involved at all; confirmed via a
minimal repro that the HTML path invokes WebView while the RTF path
does not.
@santiagobarros
santiagobarros deleted the pr/quicklook-preview branch July 11, 2026 19:57
@santiagobarros
santiagobarros restored the pr/quicklook-preview branch July 11, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant