Native Quick Look previews for developer files — JSON, Markdown, logs, source code and scripts.
Press Space in Finder. Get a readable, theme-aware preview. Zero JavaScript, zero telemetry, fully offline.
👉 loupe.celox.io — screenshots, download, checksums and FAQ on the product page
Every preview on this page is Loupe's real HTML output, rendered by its actual renderers — only the window frame is drawn around it (how). Images follow your GitHub light/dark theme.
Two of ten files from the demo. ▶ Watch the full video (77 s, MP4): JSON, broken JSON, Markdown, TSV, logs, a rhythm game chart, C#, PowerShell, a Dockerfile and an Automator workflow. The “with Loupe” side is Loupe's real output; the “without Loupe” side and the Finder are reconstructed (how).
- Why Loupe?
- Gallery — JSON · Markdown · Log files · Source code · XML · PowerShell & Batch · Charts · TSV & CSV
- Supported file types
- Installation
- Settings
- Performance
- Security & privacy
- Accessibility & theming
- How it works
- Testing
- Troubleshooting
- Contributing
- License
macOS shows most developer files in Quick Look as a grey wall of monospace text — or not at all. Loupe replaces that with previews that are built for reading:
| 🌳 JSON as a tree — collapsible, in source order, with counts and type colors. Broken files still show everything up to the error, plus a caret pointing at it. | 🪵 Logs you can scan — time, level, source and message in columns, errors tinted, stack traces kept together. Seven log formats recognized. |
| 📝 Markdown, rendered — CommonMark + GFM: tables, task lists, highlighted code, local images. | 🌈 36 languages highlighted — including PowerShell and Batch, which macOS doesn't even know as file types. |
| ⚡ Fast — a 5 MB JSON file renders in 162 ms, a 4 MB log in 338 ms. | 🔒 Safe by design — sandboxed, read-only, strict CSP, not a single byte of JavaScript, no network. |
- Order-preserving parser — keys stay in file order, duplicate keys are kept, numbers keep their spelling (
1.000stays1.000, not1). - Smart expansion — nodes open breadth-first within a budget of 300 visible rows: a
package.jsonopens completely, a 50,000-element array stays folded. - Collapsed summaries — every object and array shows its size and a peek at its first keys.
- Pure HTML — folding uses
<details>/<summary>, no script involved.
A syntax error doesn't produce a blank page: Loupe shows line, column and a source excerpt with a caret, and below it the tree up to the point of failure. The caret stays aligned with tabs, umlauts and emoji. A file that was merely cut off by Loupe's size limit is reported as a notice, never as an error.
All JSON capabilities
| Feature | Detail |
|---|---|
| Objects & arrays | Member/item count badges and a peek of the collapsed content |
| Source key order | Preserved exactly, no alphabetical re-sorting |
| Duplicate keys | Both occurrences are shown |
| Numbers | Kept as written — no float rounding, 1e400 doesn't become inf |
| Strings & escapes | \uXXXX, surrogate pairs (😀), control characters |
| Partial tree on error | Everything parsed before the error stays visible |
| Error excerpt | Line, column, context lines, caret — aligned for tabs and multi-byte characters |
| JSON Lines hint | Content after the first value is recognized as JSON Lines and explained |
| Limits | Depth 64 · 20,000 nodes · 1,000 children per container · 4 KB per string · 20 MB per file |
CommonMark and GitHub Flavored Markdown via Apple's swift-markdown: headings with anchors, emphasis, strikethrough, blockquotes, nested and numbered lists, task lists, tables with alignment, fenced code blocks with syntax highlighting and a language badge, links, and local images (embedded as data URIs, confined to the document's folder). Raw HTML is sanitized, remote images are blocked unless you allow them.
All Markdown capabilities
| Feature | Syntax | Detail |
|---|---|---|
| Headings | # … ###### |
Auto-generated anchor slugs |
| Emphasis | **bold**, *italic*, ~~strike~~ |
SF Pro typography |
| Code | `inline`, fenced ```lang |
Highlighted, with language badge |
| Blockquotes | > note |
Callout style with accent border |
| Lists | -, 1., 3. |
Nested lists, custom start numbers |
| Task lists | - [x], - [ ] |
Native-looking checkboxes |
| Tables | | a | b | |
Column alignment, zebra rows |
| Links | [t](https://…) |
Dangerous schemes (javascript:, vbscript:, data:text/html) are neutralized |
| Images |  |
Local files only, path traversal blocked, remote images off by default |
| Raw HTML | <div>…</div> |
<script>, <iframe>, <form>, event handlers etc. are stripped |
Every line is split into time · level · source · message — whatever format it's in, even when formats are mixed in one file. The level is normalized (WARN, warning, W and pino's 40 all become WARN), the toolbar counts FATAL/ERROR/WARN, and error rows are tinted together with their stack trace. URLs, IPs, numbers with units, UUIDs, paths and key=value pairs are highlighted inside messages.
| Format | Example | Extracted |
|---|---|---|
| Generic application log | 2026-09-24 17:01:02.123 INFO [main] Started |
Time, level, [source], message — also [time], time-only, [LEVEL], level=…, channel.LEVEL: (Laravel/Monolog), LEVEL:logger:msg (Python) |
| nginx / Apache access log | 203.0.113.7 - - [24/Sep/2026:…] "GET / HTTP/1.1" 503 0 … |
Client, method, path, protocol, status colored by class, size, referer, user agent · 4xx → WARN, 5xx → ERROR |
| JSON Lines | {"time":…,"level":"error","msg":"boom"} |
time/ts/@timestamp, level/severity (incl. pino numbers), msg/message, logger; everything else as key=value. Unix times become readable UTC |
| logfmt | time=… level=warning msg="disk low" |
Same fields as JSON Lines |
| syslog / journalctl | Sep 24 17:01:02 host sshd[1234]: … |
Time, host + process[pid], message — also ISO time (journalctl -o short-iso, macOS install.log) |
| macOS unified log | output of log show |
Time, process[pid], type → level (Default → NOTICE, Error → ERROR, Fault → FATAL) |
| Android logcat | 09-24 17:01:02.123 1234 5678 E Tag: … |
Time, level letter, tag, message (threadtime and brief) |
- Levels come from the head of a line only — a message that merely mentions "error" is not an error. Without a timestamp, a bare level word must be UPPERCASE or followed by
:(Info about the jobstays prose). - Big logs are read from the end. Logs grow at the bottom, so Loupe reads the last 4 MB and shows the newest 5,000 lines — with absolute line numbers (the skipped part is counted, up to 512 MB). A banner says exactly what was skipped.
- Try it:
Tests/Fixtures/logs/has one sample per format plus edge cases;python3 Scripts/generate_large_log.pycreates a 12 MB log for the tail path.
An in-process tokenizer written in Swift highlights 36 languages: Swift, Rust, Python, JavaScript, TypeScript, Go, Java, Kotlin, C, C++, PHP, Ruby, SQL, C#, F#, Visual Basic, Objective-C, Dart, Scala, Groovy, Lua, R, Perl, Elixir, Zig, AppleScript, Shell/Bash/Zsh, PowerShell, Batch, JSON, YAML, TOML/INI, XML/HTML, CSS, Dockerfile — with line numbers, a language badge and file statistics. Tokens become <span>s on the host side; nothing executes in the preview.
A dedicated, quote-aware XML tokenizer: element names, attributes, values and brackets each get their own color, as do the <?xml … ?> declaration, <!DOCTYPE …> (including an internal DTD subset), comments, CDATA sections (shown as text, not markup) and entities like & or 😀. A > inside an attribute value, comment, CDATA block or DOCTYPE never ends a tag. The same tokenizer powers HTML code blocks in Markdown, where <script>/<style> content is treated as raw text.
Besides .xml, Loupe declares a type for 13 XML dialects macOS doesn't know — .xsd, .xsl, .xslt, .xaml, .csproj, .vbproj, .fsproj, .vcxproj, .props, .targets, .resx, .wsdl, .nuspec — so Quick Look hands them to Loupe too. .svg, .rss and .plist stay with macOS (an SVG keeps showing as an image), and .storyboard/.xib/.entitlements are left to Xcode.
|
|
macOS assigns .ps1, .bat and .cmd only a dynamic type, so no Quick Look extension is ever asked to preview them. Loupe declares proper types (com.microsoft.powershell-script, com.microsoft.batch-file) and brings dedicated tokenizers:
- PowerShell —
<# help #>blocks and#comments, variables incl. scopes ($env:PATH,$script:x,${any name}), cmdlets (Get-ChildItem), parameters (-Path), word operators (-eq,-notin,-match), type literals ([string],[System.IO.File]), strings with interpolation of$varand$(…), here-strings@" … "@, size literals (10MB), case-insensitive keywords. - Batch —
REMand::comments, labels andgoto :eof, every variable form (%PATH%,%~dp0,%1,%%i,%%~nxf,!delayed!,%DATE:~0,4%), switches (/b,/a), case-insensitive keywords (IF NOT EXIST,EQU,ERRORLEVEL).
Clone Hero / Moonscraper charts are shown section by section — [Song], [SyncTrack], [Events] and every track like [ExpertSingle]. Every section starts collapsed; its summary line says what's inside (4811 Noten · 36 Star Power · 4 Ereignisse), and the toolbar names artist and title from [Song]. Opened, each row shows the tick and the song time (computed from Resolution and every tempo change), the entry type and a readable value: notes in their fret colors (green, red, yellow, blue, orange; Force/Tap/Open flags) with sustain, Star Power phrases, section markers, tempo as BPM and time signatures like 6/8.
- macOS has no type for
.chart— Loupe declaresio.celox.loupe.chart(derived from plain text), otherwise Quick Look would never ask it. - A file with the same extension that isn't a chart is shown as plain text instead of failing.
- Limits: 5,000 rows per section, 30,000 in total,
.chartfiles are read up to 8 MB. Typical charts are far below that (a full five-difficulty chart: ~27,000 lines, 0.5 MB).
Tab-separated and other delimited files become a proper table: sticky header, sticky row numbers, numbers right-aligned with tabular digits, delimiter auto-detection (, ; \t), RFC 4180 quoting incl. multi-line cells, and a summary bar. Limits: 2,000 rows × 200 columns.
Important
Comma-separated .csv files are not previewed by Loupe — and no Quick Look extension can change that. macOS routes public.comma-separated-values-text to its built-in /System/Library/QuickLook/Office.qlgenerator, which takes precedence over every third-party extension and ignores Dark Mode. Loupe registers the type, yet Quick Look never calls it for .csv (verified in the unified log; a .tsv in the same session reaches Loupe). Declaring an own type for .csv doesn't help either — Launch Services keeps Apple's. The generator lives on the SIP-protected system volume. The same wall has been hit by CSV plugins since macOS 10.15 (p2/quicklook-csv#26). Workaround: save tabular data as .tsv.
What matters is not what Loupe can render, but what Quick Look actually hands to Loupe. This table is checked against the extension's registration by a unit test (DocsSyncTests).
| Format | Extensions | Type (UTI) | Finder preview |
|---|---|---|---|
| JSON | .json |
public.json |
✅ |
| Markdown | .md .markdown |
net.daringfireball.markdown |
✅ |
| Log | .log |
com.apple.log → public.log |
✅ |
| Tab-separated | .tsv |
public.tab-separated-values-text |
✅ |
| Comma-separated | .csv |
public.comma-separated-values-text |
❌ reserved by macOS, see above |
| PowerShell | .ps1 .psm1 .psd1 |
com.microsoft.powershell-script (declared by Loupe) |
✅ |
| Batch | .bat .cmd |
com.microsoft.batch-file (declared by Loupe) |
✅ |
| Swift · Rust · Go | .swift .rs .go |
public.swift-source · org.rust-lang.rust-script · org.golang.go-script (imported by Loupe) |
✅ |
| Python · Ruby · PHP | .py .pyw .rb .php |
public.python-script (.pyw: io.celox.loupe.python-windows-script (declared by Loupe)) · public.ruby-script · public.php-script |
✅ |
| JavaScript · JSX/TSX | .js .mjs .cjs .jsx .tsx |
com.netscape.javascript-source · io.celox.loupe.commonjs-source · io.celox.loupe.jsx-source (declared by Loupe) |
✅ |
| Java · Kotlin | .java .kt .kts |
com.sun.java-source · org.kotlinlang.source (imported by Loupe) |
✅ |
| C · C++ | .c .h .cpp .cc .cxx .hpp .hxx .h++ |
public.c-source · public.c-plus-plus-source · headers |
✅ |
| Shell & Terminal | .sh .bash .zsh .command .tool .fish .ksh .csh .tcsh .zsh-theme .env |
public.shell-script and variants (.command/.tool: com.apple.terminal.shell-script; .fish: io.celox.loupe.fish-script; .zsh-theme: io.celox.loupe.zsh-theme; .env: io.celox.loupe.env) |
✅ |
| AppleScript | .applescript |
com.apple.applescript.text |
✅ |
| Workflow | .workflow .wflow |
com.apple.automator-workflow · com.apple.shortcuts.workflow-file |
✅ |
| SQL | .sql |
org.iso.sql (imported by Loupe) |
✅ |
| YAML · TOML · INI | .yaml .yml .toml .ini |
public.yaml · public.toml · com.microsoft.ini (imported by Loupe) |
✅ |
| XML | .xml |
public.xml |
✅ |
| XML dialects | .xsd .xsl .xslt .xaml .csproj .vbproj .fsproj .vcxproj .props .targets .resx .wsdl .nuspec |
io.celox.loupe.xml-document (declared by Loupe) |
✅ |
| CSS · SCSS · Sass · Less | .css .scss .sass .less |
public.css · io.celox.loupe.stylesheet-source (declared by Loupe) |
✅ |
| C# · F# · Visual Basic | .cs .csx .fs .fsx .fsi .vb |
com.microsoft.c-sharp (imported by Loupe) · io.celox.loupe.fsharp-source · io.celox.loupe.visual-basic-source (declared by Loupe) |
✅ |
| Lua · Dart · Scala | .lua .dart .scala .sc |
org.lua.lua-source · dev.dart.dart-script · org.scala-lang.scala-source (imported by Loupe) |
✅ |
| Groovy · Gradle | .groovy .gradle |
io.celox.loupe.groovy-source (declared by Loupe) |
✅ |
| Objective-C | .m .mm |
public.objective-c-source · public.objective-c-plus-plus-source |
✅ |
| R · Perl | .r .pl .pm |
com.apple.rez-source (macOS thinks .r is Rez; Loupe goes by the extension) · public.perl-script |
✅ |
| Elixir · Zig | .ex .exs .zig |
io.celox.loupe.elixir-source · io.celox.loupe.zig-source (declared by Loupe) |
✅ |
| Dockerfile | .dockerfile |
io.celox.loupe.dockerfile (declared by Loupe) |
✅ |
| Rhythm game chart | .chart |
io.celox.loupe.chart (declared by Loupe) |
✅ |
Highlighted, but not reachable from Finder:
.ts— macOS types it as an MPEG-2 transport stream (video). Claiming it would turn real video files into text.
Imported by Loupe means macOS itself doesn't know the type — only editors like CotEditor or BBEdit bring it along. Without them .rs or .cs would get a dynamic type and Quick Look would never ask Loupe. Loupe uses the same identifier those editors do, so the two don't clash. Declared by Loupe marks types that have no common identifier at all.
The same is true for .out, .err and rotated logs like app.log.1: dynamic types — and claiming .out would additionally turn binaries like a.out into text.
- Download
Loupe-vX.Y.Z-macOS.zipfrom loupe.celox.io or the latest release — builds are Apple Silicon (arm64). Each release ships aSHA256SUMS.txt. - Unzip and move
Loupe.appto/Applications. - The app is ad-hoc signed, not notarized. On first launch macOS will refuse to open it — either right-click → Open, use System Settings → Privacy & Security → Open Anyway, or run:
xattr -dr com.apple.quarantine /Applications/Loupe.app
- Launch Loupe once. The window shows whether the Quick Look extension is registered.
Requires Xcode 16 / Swift 6 command line tools. No Xcode project needed.
git clone https://github.com/pepperonas/loupe.git
cd loupe
./Scripts/install_app.sh # build (release), sign ad-hoc, install to /Applications, register, reset Quick LookScripts/build_app.sh [debug|release] only builds the bundle into build/Loupe.app; Scripts/package_release.sh vX.Y.Z creates the release zip plus checksums.
Third-party Quick Look extensions need a one-time approval:
- macOS 15 and later: System Settings → General → Login Items & Extensions → Quick Look → enable Loupe.
- macOS 14: System Settings → Privacy & Security → Extensions → Quick Look → enable Loupe.
- Or from the terminal:
pluginkit -e use -i io.celox.loupe.preview
Then select a file in Finder and press Space.
The companion app (Loupe.app) lets you switch Loupe on and off and tune how previews look. Changes apply the next time you press Space — no restart needed.
Previews on/off
| Switch | Covers |
|---|---|
| Loupe-Vorschauen aktiv (global) | Every preview below. Off = Loupe stays out of the way entirely. |
| Markdown | .md, .markdown |
| JSON | .json |
| Tabellen (TSV) | .tsv, .tab |
| Log-Dateien | .log |
| Code | All source languages, scripts (PowerShell, Batch, shell), XML and config files |
| Charts (.chart) | .chart (Clone Hero / Moonscraper) |
When Loupe is switched off — globally or for a category — you see the plain text, exactly as on a Mac without Loupe. The per-category choices are kept while the global switch is off.
Appearance
| Setting | Options | Applies to |
|---|---|---|
| Appearance (Erscheinungsbild) | System · Light · Dark | All previews |
| Text size (Textgröße) | Small · Standard · Large | All previews |
| Markdown width (Markdown-Breite) | Compact 680 px · Standard 840 px · Wide 1040 px · Full width | Markdown |
App and extension share these settings through the preference domain io.celox.loupe.shared (sandbox exception shared-preference: the app may write, the extension may only read). Up to 0.4.0 the settings never reached the extension — see the changelog.
Language
| Setting | Options | Applies to |
|---|---|---|
| Language (Sprache) | System · English · Deutsch | The companion app's window and menu |
System follows macOS: German when German comes first in your preferred languages, English otherwise. Switching takes effect immediately. The Quick Look previews themselves are not translated — their labels ("12 Schlüssel", "25 Zeilen") stay as they are, in every language setting.
About
The bottom of the app shows the version, the developer (Martin Pfeffer, celox.io), buttons for the source code on GitHub, the website and a PayPal donation, and the MIT License.
Measured by the test suite (release build, Apple M1 Pro, macOS 27). The suite enforces hard upper bounds for the large cases — 1 s in release builds, 2.5 s in debug builds on shared CI runners — so a performance regression fails the build.
| Input | Renderer | Time |
|---|---|---|
| Small JSON document | JSON tree | 8 ms |
| 5 MB JSON | JSON tree (node limit applies) | 162 ms |
| 4 MB log (≈ 40,000 lines → newest 5,000) | Log table | 338 ms |
8 MB .chart (largest readable, ≈ 470,000 lines → 30,000 rows) |
Chart sections | 450 ms |
Large files are never read completely: JSON and code are capped at 20 MB from the start, .chart at 8 MB, logs are read from the end (4 MB).
Developer files often come from untrusted sources (git clone, downloads, build artifacts). Loupe treats every file as hostile input:
- Sandboxed & read-only — the extension runs in the App Sandbox with
com.apple.security.files.user-selected.read-only, nothing else. - No JavaScript, anywhere — previews are static HTML + CSS. Collapsing uses
<details>. - Strict Content Security Policy —
default-src 'none'; style-src 'unsafe-inline'; img-src 'none'(Markdown:img-src data: cid:). No policy allowsscript-src. - Everything is escaped — every value, key, log field, file name and code token goes through HTML escaping; tests feed
<script>andonerror=through every renderer. - Markdown sanitizing — dangerous elements and event handlers are stripped,
javascript:/vbscript:/data:text/htmllinks neutralized. - Path traversal guard — local images are resolved with symlinks canonicalized and must stay inside the document's folder.
- No network — remote images are blocked by default (no tracking pixels), there is no telemetry and no update check.
- Hard limits against hostile files — JSON depth 64 (stack-overflow guard), 20,000 nodes, 1,000 children per container, 4 KB per displayed string, 20 MB per file; logs 4 MB / 5,000 lines; tables 2,000 rows × 200 columns.
Light and dark themes follow the system (or your choice in the app) and use SF Pro / SF Mono. Every color role meets WCAG AA (≥ 4.5 : 1):
- JSON, Markdown and code colors were measured on a canvas, composited over their real backgrounds (
Scripts/measure_contrast.html) — ratios from 6.4 : 1 to 16.8 : 1. - Log colors (all levels, HTTP status classes, timestamps, sources) are checked by a unit test in both themes — including on the tinted background of error rows.
Measured JSON/Markdown color roles
| Role | Light (#ffffff) |
Dark (#1e1e1e) |
|---|---|---|
| Text | #1d1d1f · 16.83 : 1 |
#f5f5f7 · 15.31 : 1 |
| Dimmed text | #5b5e69 · 6.46 : 1 |
#a1a1a6 · 6.48 : 1 |
| Object key | #0b5fb0 · 6.41 : 1 |
#7ab8ff · 8.04 : 1 |
| String | #b3261e · 6.54 : 1 |
#ff8170 · 6.85 : 1 |
| Number | #1c00cf · 10.77 : 1 |
#dabaff · 9.88 : 1 |
| Boolean | #7a3ea3 · 6.90 : 1 |
#d8a0ff · 8.25 : 1 |
| Keyword | #af00db · 6.42 : 1 |
#ff7ab2 · 8.12 : 1 |
| Link | #0066cc · 6.82 : 1 |
#2997ff · 7.84 : 1 |
| Error banner | #a5251c · 6.72 : 1 |
#ff8a80 · 6.64 : 1 |
Negative control: #cccccc on #ffffff measures 1.61 : 1, proving the harness catches failures.
flowchart LR
F["Finder<br/>(Space)"] --> Q["Quick Look<br/>daemon"]
Q -->|file URL| P["LoupePreview.appex<br/>PreviewProvider"]
P --> R["RendererRegistry<br/>extension → UTI"]
R --> J["JSON"] & M["Markdown"] & L["Log"] & C["TSV/CSV"] & S["Source code"]
P -.->|read strategy| FR["PreviewFileReader<br/>head · tail"]
J & M & L & C & S --> H["HTML + CSS<br/>strict CSP, 0 JS"]
H -->|QLPreviewReply| Q
- Finder asks Quick Look for a preview; Quick Look hands the file URL to the extension because its type is listed in
QLSupportedContentTypes. - The
RendererRegistrypicks a renderer by file extension first, then by type (UTI). - The renderer declares how to read the file: from the start (documents, code) or from the end (logs);
PreviewFileReaderreads only that part. - The renderer returns a self-contained HTML page (styles inline, strict CSP); Quick Look displays it in WebKit.
Project layout
Loupe.app Companion app (AppKit): status, settings
└── Contents/PlugIns/LoupePreview.appex Quick Look extension (QLPreviewProvider)
Sources/
├── Loupe/ Companion app
├── LoupePreview/ Extension entry point + Info.plist (QLSupportedContentTypes)
└── LoupeCore/ Everything testable
├── JSON/ Lexer, order-preserving parser with recovery, source excerpts
├── Markdown/ swift-markdown visitor, sanitizer, safe image resolver
├── CSV/ RFC 4180 parser with delimiter detection, table renderer
├── Log/ Log model, format detection, message highlighter
├── Highlighting/ Tokenizers for 36 languages (incl. PowerShell & Batch)
├── Chart/ .chart parser and tempo map → song time
├── Preview/ Renderer protocol, registry, file reader, one renderer per format
├── Render/ JSON tree, expansion planner, HTML escaping
├── Theme/ CSS generator (light/dark, WCAG AA)
├── Configuration/ Settings shared with the extension
└── Utilities/ Extension status checker (pluginkit)
Tests/LoupeTests/ Test suite (swift run LoupeTests)
Tests/Fixtures/ Sample files: JSON, Markdown, logs/, scripts/
Tools/ScreenshotGenerator/ Generates the mockups on this page
Scripts/ Build, install, package, badges, large-log generator
swift run LoupeTests # debug
swift run -c release LoupeTests # release (as in the release workflow)392 tests in a dependency-free harness, run by CI on every push:
| Area | Tests | Highlights |
|---|---|---|
| JSON | 70 | Order, duplicates, number spelling, recovery, limits incl. depth bomb, caret alignment |
| Log files | 64 | All seven formats, level normalization, stack traces, CRLF, tail reading, absolute line numbers, WCAG contrast |
| Highlighting | 78 | 36 languages, quote-aware XML/HTML, PowerShell & Batch, C# verbatim strings, every non-system file type declared so Quick Look actually asks Loupe, no phantom last line, every printable character in every language must terminate and round-trip |
| Markdown & safety | 41 | GFM, sanitizer bypass attempts, path traversal, remote-image blocking |
| TSV / CSV | 22 | RFC 4180 quoting, delimiter detection, limits |
| Theming & settings | 40 | Light/dark CSS, settings migration, on/off switches per category, plain-text fallback encodings, shared preference domain + entitlements |
| Languages & about | 14 | English/German app texts, language resolution, previews byte-identical in every language, every About link pinned to its real target |
Charts (.chart) |
31 | Sections, BOM/CRLF, broken files, tempo map → song time, every section starts collapsed, fret colors, escaping, row and read limits, WCAG contrast, type registration |
| Registry & app | 19 | Type routing, CSP, invalid UTF-8, empty files, pluginkit parsing incl. sandbox refusal |
| Docs sync | 6 | This README against the code: versions, test badge, image paths, EN/DE parity, Finder reachability |
| Performance | 4 | Hard time limits for large inputs |
How the tests are kept honest:
- Mutation-checked. New tests are verified by deliberately breaking the code they guard — a test that stays green against broken code is rewritten. This is how several initially blind tests were found and sharpened.
- Documentation is tested.
DocsSyncTestsfails when this README promises a file type Quick Look never delivers, when the test badge is stale, or when the English and German READMEs drift apart.Scripts/update_readme_stats.shrefreshes the badges. - Fixtures for manual testing live in
Tests/Fixtures/logs/andTests/Fixtures/scripts/.
| Symptom | Fix |
|---|---|
| Finder still shows plain text | Enable the extension (see above), then qlmanage -r && qlmanage -r cache && killall Finder |
| Only plain text appears | Loupe may be switched off — open Loupe.app and check the global switch and the category |
| Is the extension registered? | pluginkit -m -v -i io.celox.loupe.preview — a leading + means enabled |
| Old version keeps answering | List all copies: pluginkit -m -A -v -p com.apple.quicklook.preview | grep -i loupe, remove stale ones, reinstall with ./Scripts/install_app.sh |
| Which type does macOS assign a file? | mdls -name kMDItemContentType <file> — dyn.… means no extension will be asked |
.csv is still white |
Expected — macOS reserves CSV, see above. Use .tsv. |
| Watch the extension live | log stream --predicate 'subsystem == "io.celox.loupe.preview"' --info — logs the chosen renderer and output size for every preview |
Issues and pull requests are welcome.
swift run LoupeTestsmust stay green — new behavior comes with tests (and ideally a failing test first).- User-visible changes go into
CHANGELOG.md(Keep a Changelog, SemVer). - Changed rendering? Regenerate the mockups:
Tools/ScreenshotGenerator/generate.sh(needs Google Chrome;pngquantoptional). - Changed test count or code size?
Scripts/update_readme_stats.sh. - Keep
README.mdandREADME.de.mdin step — the docs-sync test checks it.
The images on this page are generated by Tools/ScreenshotGenerator: a small Swift tool renders the sample files through Loupe's real RendererRegistry and renderers (light and dark), a Python script places each result into a macOS-style Quick Look window, and headless Chrome captures it at 1.5× resolution. The window frame and background are drawn; the content is Loupe's unmodified output.
The demo video uses the same principle: the Loupe side of every scene is the unmodified output of the real renderers (dark appearance). The Finder window, the cursor and the “without Loupe” side are drawn. What macOS shows without Loupe follows from the file type macOS reports: types it knows as text (.json, .md, .tsv, .log) appear as plain text; types only Loupe declares (.cs, .ps1, .chart), files without an extension (Dockerfile) and Automator workflows appear as a file icon.
If Loupe saves you time, consider supporting its development:
MIT — see LICENSE. Made with ❤️ by Martin Pfeffer · celox.io · © 2026
