Quick Look preview for Jupyter notebooks on macOS.
Select an .ipynb in Finder and press Space — rendered markdown, syntax-highlighted code, and cell outputs.
macOS deprecated the old .qlgenerator Quick Look plugins (the C/Obj-C
generator API). The supported replacement is a Quick Look Preview
Extension (QLPreviewingController) shipped as an app extension inside a
host app, written in Swift. So this project is:
QLJupyter.app— a tiny host app. Its only job is to carry the extension and to declare the.ipynbfile type to the system.JupyterPreview.appex— the Quick Look extension. It parses the notebook JSON, builds an HTML representation, converts that to anNSAttributedString, and displays it in anNSTextView.
Why NSTextView and not WKWebView? Inside the sandboxed Quick Look extension,
WKWebView's out-of-process web content renderer never completes navigation, so the preview comes up blank. Rendering the HTML into an attributed string is fully in-process and reliable. A customNSLayoutManagerdraws the rounded code/output "cards".
- Markdown cells — headings, bold/italic, inline & fenced code, lists, blockquotes, links, images, and tables (self-contained converter, no JS).
- Code cells —
In [n]:/Out[n]:prompts and Python syntax highlighting (keywords, strings, comments, numbers, builtins, functions). - Outputs — stdout/stderr streams,
text/plain, embeddedtext/html(scripts stripped), inline PNG/JPEG/GIF/SVG images (base64 data URIs), and tracebacks with ANSI color codes stripped for readability. - Rounded, equal-width code/output cards; light/dark aware; fully offline.
Grab QLJupyter.zip from the latest release, then:
# The app is ad-hoc signed (not notarized), so macOS quarantines downloads.
# Remove the quarantine flag or the Quick Look extension won't load:
xattr -dr com.apple.quarantine ~/Downloads/QLJupyter.app
mv ~/Downloads/QLJupyter.app /Applications/
open /Applications/QLJupyter.app # launch once to register the extension
qlmanage -r # refresh Quick LookThen select an .ipynb file in Finder and press Space. If you'd rather not
run a prebuilt binary, build it yourself below — a local build is trusted
automatically and needs no xattr step.
Requires Xcode 15+ on macOS 13 (Ventura) or later.
# Open in Xcode and Run the "QLJupyter" scheme, or from the terminal:
xcodebuild -project QLJupyter.xcodeproj -scheme QLJupyter -configuration Release build
# Move the built app somewhere permanent so the extension stays registered:
cp -R build/Build/Products/Release/QLJupyter.app /Applications/
open /Applications/QLJupyter.app # launch once to register the extensionThe extension is registered with the system the first time the host app is
launched from /Applications. To force a refresh:
qlmanage -r
qlmanage -r cache- The extension supports the
org.jupyter.ipynbandcom.jupyter.notebookcontent types (whichever a given Mac resolves.ipynbto). It deliberately does not claimpublic.json, so it won't hijack previews of every JSON file. - If a preview doesn't appear, confirm the app is in
/Applications, launch it once, then runqlmanage -r. Gatekeeper may require you to allow the app on first launch since it's ad-hoc signed.
Because the app isn't notarized, the first launch is blocked by Gatekeeper.
Instead of the xattr command you can approve it from the UI:
- Double-click the app (macOS shows a "can't be opened" / "not opened" dialog — just dismiss it).
- Open System Settings → Privacy & Security, scroll to the Security section, and click Open Anyway next to "QLJupyter was blocked".
- Confirm with Open. This clears the quarantine, so the Quick Look
extension can load. Then run
qlmanage -r(or log out/in) and press Space on an.ipynb.
You only have to do this once.
.ipynb is a popular file type, and several tools register their own Quick
Look handlers for it (Syntax Highlight / sbarex, ipynb-quicklook and other
legacy generators, JSON previewers, …). When more than one handler claims the
same file type, macOS picks one, and there is no public API to force yours
to win. If Space shows raw JSON, a syntax-highlighted blob, or the wrong style,
another handler is winning. Here's how to find and fix it.
mdls -name kMDItemContentType -name kMDItemContentTypeTree /path/to/notebook.ipynbThis tells you the resolved UTI (commonly org.jupyter.ipynb, sometimes
public.json). Whatever it is, the winning handler is one that claims that
type or a parent of it.
# All modern Quick Look preview extensions (ours is com.example.QLJupyter.JupyterPreview):
pluginkit -mAvvv -p com.apple.quicklook.preview
# Is ours registered and enabled? A leading "+" means user-enabled.
pluginkit -m -i com.example.QLJupyter.JupyterPreviewTo see which extension actually runs, trigger a preview and look for the extension process that spawns:
qlmanage -p /path/to/notebook.ipynb >/dev/null 2>&1 &
sleep 2
ps -Axo pid,comm | grep -iE 'JupyterPreview|Quick Look|quicklook' | grep -v grep
killall qlmanage 2>/dev/nullIf you see e.g. Syntax Highlight Quick Look Extension, that's your culprit.
Legacy .qlgenerator plugins don't spawn a separate process (they load inside
quicklookd), so also check for those:
# Legacy generators live in these folders and inside app bundles:
ls -d /Library/QuickLook/*.qlgenerator ~/Library/QuickLook/*.qlgenerator 2>/dev/null
find /Applications -maxdepth 4 -name '*.qlgenerator' 2>/dev/null | grep -i ipynb
# What claims org.jupyter.ipynb in Launch Services:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep -B3 -i 'org.jupyter.ipynb'A competing modern extension (e.g. Syntax Highlight) — disable its Quick Look, which is fully reversible:
pluginkit -e ignore -i org.sbarex.SourceCodeSyntaxHighlight.QuickLookExtension
# re-enable later with:
pluginkit -e use -i org.sbarex.SourceCodeSyntaxHighlight.QuickLookExtensionYou can also toggle handlers in System Settings → General → Login Items & Extensions → Quick Look.
A legacy .qlgenerator — it can't be toggled with pluginkit; move it out
of the way (reversible) and unregister it:
GEN="/Applications/Jupyter Notebook Viewer.app/Contents/Library/QuickLook/ipynb-quicklook.qlgenerator"
mkdir -p ~/QL-disabled && mv "$GEN" ~/QL-disabled/
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -u "$GEN"After any change, make sure ours is enabled and the app is in
/Applications, then refresh the cache:
pluginkit -e use -i com.example.QLJupyter.JupyterPreview
qlmanage -r ; qlmanage -r cache ; killall FinderRe-open the preview (close any stale Quick Look window first). If it's still wrong, disable competitors one at a time using step 2 to confirm which one takes over each time.
MIT © 2026 Yike Ye. See LICENSE.
Host/ Host app (SwiftUI) + Info.plist declaring the UTI
JupyterPreview/ Quick Look extension
PreviewViewController.swift QLPreviewingController + NSTextView
NotebookRenderer.swift nbformat JSON -> inline-styled HTML
Markdown.swift minimal Markdown -> HTML (incl. tables)
Highlighter.swift Python syntax highlighter
Palette.swift light/dark color palettes
ANSI.swift strip ANSI escapes from tracebacks
RoundedBackgroundLayoutManager.swift rounded, equal-width code/output cards