Skip to content

j-256/qlomni

Repository files navigation

QLOmni

A macOS QuickLook Preview Extension that previews the text files macOS itself doesn't.

What it fixes

Press space on a .txt file and macOS shows you the contents. Press space on a few common file types and you get a generic icon and "Document – 4 bytes" instead. The most common cases:

  • Extensionless executables (e.g. myscript, a shell script saved without .sh) – tagged public.unix-executable, which has no QuickLook handler.
  • Files tagged directly as public.data – extensionless non-executables (a notes file named shopping-list) and dot-prefix-only filenames with no further dot (.gitignore, .bashrc, .htaccess, .vimrc). Launch Services has nothing to fingerprint, so it tags them with the system's most generic UTI; no other handler claims it.
  • Files with extensions macOS doesn't recognize – including AI-agent staples .md, .jsonl, and .output, plus .jsonc, .har, .tsx, .editorconfig, .tf, .graphql, common config formats, and source files for languages whose extensions aren't bundled with macOS (Rust, Go, Kotlin, etc.). See SUPPORTED.md for the full list.
  • Environment-variant configs.env.production, docker-compose.yml.example, nginx.conf.staging, database.yml.test, etc. The trailing variant suffix becomes the file's extension as far as macOS UTI lookup is concerned, and QLOmni declares a UTI for each common one (.example, .sample, .local, .development, .dev, .production, .prod, .staging, .test). See DESIGN.md § environment-variant suffixes for the rationale.
  • YAML (.yaml, .yml), TOML (.toml), INI (.ini), CSS (.css), and NDJSON (.ndjson) – have UTIs that conform to public.text but not public.plain-text. The system text generator only handles public.plain-text, so they fall through.

QLOmni handles all of these – with one notable exception.

Doesn't fix .ts. TypeScript files have a hard problem: macOS itself (CoreTypes, the bundled type registry) tags every .ts file as public.mpeg-2-transport-stream (an MPEG-2 video container – .ts predates TypeScript as a video extension). QuickLook routes that UTI to the bundled Movie display bundle, which sits ahead of any third-party Preview Extension and cannot be displaced. .ts files won't preview as text on any modern macOS, with or without QLOmni. .tsx is unaffected and previews fine. See DESIGN.md § the system display bundle trap.

Already covered by macOS

If you came here looking for an extension that isn't in the list above, check whether macOS already handles it before assuming you need QLOmni. Some that surprise people:

  • Logs and diffs: .log, .diff, .patch all conform to public.plain-text and route through the system text generator.
  • Scripts with conventional extensions: .sh, .bash, .zsh, .py, .rb, .pl, .swift, .lua, .r – same.
  • Tabular: .csv and .tsv get dedicated handlers (Apple's bundled Office.qlgenerator – not to be confused with Microsoft Office – and the system text generator respectively).

To find out whether macOS already covers a given extension on your machine:

mdls -name kMDItemContentType -name kMDItemContentTypeTree yourfile.foo

If kMDItemContentType is a real UTI (not dyn.*) and kMDItemContentTypeTree includes public.plain-text, the system should preview it. If that's true and pressing space still doesn't show a preview, the issue is at the QuickLook dispatch layer, not the UTI layer – different problem than QLOmni solves. (Some UTIs that conform to public.plain-text still route to dedicated generators that may not render cleanly in all contexts; .xml is one such case.)

How

Two pieces, both shipped in a single bundle:

  • A Preview Extension (.appex) that handles public.unix-executable, public.yaml, public.toml, com.microsoft.ini, public.css, and public.data / public.content directly – rendering each as plain text. (public.content is a supertype of public.data; both are listed for belt-and-suspenders coverage of files macOS tags with the bare wildcard.) Binary content is detected in-process via a NUL byte check and falls through to the system "no preview" placeholder rather than rendering garbage.
  • A set of UTI declarations for common formats macOS doesn't natively know about. Most extensions get assigned a plain-text-conforming UTI and route through the system text generator unchanged; QLOmni's role is just making sure the file gets a sensible UTI.

For the technical details – including why some plausible approaches don't work – see DESIGN.md.

Install

Requires macOS 12 (Monterey) or later.

Pre-built (recommended)

Download the latest QLOmni.app.zip from the Releases page, unzip, and drag QLOmni.app into /Applications/.

Releases are ad-hoc signed (not notarized), so on first launch Gatekeeper will block the app. Either right-click → Open the first time, or strip the quarantine attribute:

xattr -dr com.apple.quarantine /Applications/QLOmni.app

Build from source

Requires Xcode.app (the full IDE, not just the Command Line Tools).

git clone https://github.com/j-256/qlomni.git
cd qlomni
make install

This builds a universal binary (arm64 + x86_64), ad-hoc signs it, copies QLOmni.app to /Applications/, registers the Preview Extension with PluginKit, and resets QuickLook so the changes take effect immediately. Locally-signed builds aren't subject to Gatekeeper, so no quarantine step needed.

To bake personal extensions into your local build that aren't worth shipping upstream, see Building with extra extensions.

Uninstall

rm -rf /Applications/QLOmni.app
qlmanage -r && qlmanage -r cache

Note that this removes both the Preview Extension and the UTI declarations – files that were resolving to a real UTI (e.g. user.jsonc) will revert to a synthetic dyn.* type after the next Launch Services scan, and lose preview support along with it. public.data-tagged files (extensionless non-executables, dotfiles like .gitignore – see DESIGN.md § files tagged directly as public.data for why they end up with that UTI) will also stop previewing, since they were routing through the appex's public.data claim rather than getting a UTI from the host plist.

Building with extra extensions

If you want to preview extensions niche enough that they don't belong in the upstream release – internal-format .foo files, an obscure DSL only your team uses – pass EXTRA_EXTS=<file> to make build or make install:

make install EXTRA_EXTS=~/Documents/qlomni-extras.txt

The file is one entry per line, ext | Description, with # comments and blank lines ignored:

# personal extras
nfo   | NFO release notes
sigil | Sigil package source

Each line becomes a plain-text-conforming UTI in the local bundle, namespaced as user.qlomni-ext.<ext> so it can't collide with anything QLOmni ships. The committed plist isn't modified; the file lives outside the repo, persists across git pull / clean checkouts, and make build with no EXTRA_EXTS cleanly reverts the bundle to the shipped set. Refuses to override an extension already declared by QLOmni and refuses to run inside make release, so you can't accidentally taint a release artifact.

This is build-from-source only – pre-built binaries from the Releases page cannot pick up local extras since UTIs are baked into the bundle at build time.

Verify

After installing, check that the Preview Extension is registered:

pluginkit -m -p com.apple.quicklook.preview | grep qlomni

Should print:

+    dev.j-256.qlomni.QLOmniExtension(1.0.0)

The leading + means it's enabled. Then test against any of the formats listed above.

If you've built and installed QLOmni multiple times, Launch Services may accumulate stale registrations pointing at old build paths (DerivedData, prior /Applications/QLOmni.app versions, etc.). Symptoms: pluginkit -m -p com.apple.quicklook.preview | grep qlomni prints multiple entries, or QuickLook routes to a phantom build. To clean them up:

make purge-ls

This unregisters every QLOmni-related path Launch Services knows about except /Applications/QLOmni.app, then refreshes the live install's registration. Sparing the live install during the unregister pass avoids a brief window where QuickLook has no QLOmni at all (Finder kind labels flicker, in-flight previews can fail); the final refresh ensures the live install's LS entry reflects current bundle metadata.

Tests

make test              # Swift unit tests (PreviewRenderer truncation/IO)
make install           # required before integration tests
make test-integration  # asserts mdls returns the expected UTI for each declared extension

make test is hermetic – it builds and runs without touching /Applications, and runs in CI on every push to main and every pull request. make test-integration requires QLOmni to be installed (it asks PluginKit and Launch Services about the live system) and asks mdls what UTI each fixture in integration/fixtures/ resolves to. It does not run in CI – mdls and PluginKit aren't reliable on headless runners. Two assertion modes:

  • Strict – extensions where no other declarer is expected to compete. The fixture must resolve exactly to the UTI QLOmni declared.
  • Lenient – extensions where another bundle may legitimately also claim them (e.g. .ts vs CoreTypes' MPEG-2, .gs vs Xcode's GLSL shader). Any non-dyn.* UTI passes; the harness reports who won.

It does not assert rendering correctness – that requires qlmanage -p <fixture> and a human eye. In particular, "Lenient passed" doesn't mean the file previews on this machine, only that some real UTI got assigned.

Investigating UTI dispatch

Two helpers under tools/ for poking at how the system resolves a given extension or file:

./tools/uti.swift rs ini gs              # live LaunchServices lookup per extension
./tools/mdls-summary.sh some-file.foo    # one-line mdls summary for a path

uti.swift queries the live LaunchServices API and is the right tool right after a registration change (lsregister -u/-f). mdls reads from a Spotlight metadata cache that can stay stale for minutes after registrations change, so prefer uti.swift when investigating contested extensions.

Limitations

  • Plain text rendering only – no syntax highlighting, no pretty-printing.
  • Files larger than 1 MiB are truncated.
  • Multi-extension files like .env.integration.stg (last segment .stg, not declared) aren't routable – UTI lookup keys on the substring after the last dot. Files where the last segment is declared (e.g. .env.production.localuser.local) preview fine. This is a platform limitation, not a QLOmni one. See DESIGN.md § multi-extension files.
  • Files with an extension that isn't declared anywhere on your machine resolve to a synthetic dyn.* UTI. QuickLook dispatches by concrete UTI, not via the conformance tree, so wildcard claims like public.data don't catch them. To preview such files, the extension must be declared specifically (which is what every entry in SUPPORTED.md does). This is distinct from the no-extension case – shopping-list (no extension at all) does preview, since macOS tags it directly as public.data. See DESIGN.md § how wildcard-UTI claims actually dispatch.
  • Some extensions QLOmni deliberately doesn't declare because they have no consistent shape. .tmp is the obvious case: vim swap files and notes are text, but Word autosaves, Excel scratch files, partial downloads, and Photoshop history snapshots are binary. Declaring .tmp as plain-text would briefly try to decode every binary .tmp file before falling back to the "no preview" placeholder. If you need previews for a specific text-shaped extension that isn't declared, file an issue with the extension and what kind of files actually use it. Workaround: rename or symlink with a known extension.
  • .ts caveat above is also a limitation, not a bug.

Contributing

See CONTRIBUTING.md for how to run tests, cut a release, and use the CI workflow's manual modes (test / dry-run / release).

License

MIT – see LICENSE.

Acknowledgements

Inspired by QLStephen. For differences from QLStephenSwift – another modern descendant of QLStephen – see COMPARISON.md.

About

macOS QuickLook Preview Extension for files macOS itself doesn't preview

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors