quicklook-kit
v0.7.0
Published
Build Quick Look previews for macOS and Windows with Vite-based web frameworks
Maintainers
Readme
quicklook-kit
Build Quick Look previews for macOS and Windows with Vite-based web frameworks, no Swift or C# required
Your preview is a web page. quicklook-kit builds it with your framework and wraps it in a small native host: a Quick Look extension on macOS, a QuickLook plugin on Windows. You never touch Swift, C# or Xcode.
Built-in support for SvelteKit, Astro, SolidStart, Nuxt and Analog. Any other framework that builds with Vite and writes a static index.html works through an integration.
Requirements
- Node.js 22.17 or later, which runs the
quicklookCLI - For macOS: macOS 14 or later, with Xcode and XcodeGen installed
- For Windows: the .NET SDK, on any OS. Testing the plugin needs Windows with QuickLook and the Microsoft Edge WebView2 Runtime, which ships with Windows 11
Installation
npm install --save-dev quicklook-kitTo start from scratch, run pnpm create quicklook instead.
Setup
1. Configure
Create quicklook.config.ts in your project root:
import { defineConfig } from 'quicklook-kit';
export default defineConfig({
name: 'Quick Look Example',
macos: {
bundleId: 'com.example.QuickLookExample',
types: [
{
id: 'com.example.QuickLookExample.example',
description: 'Example File',
extensions: ['example'],
conformsTo: ['public.plain-text'],
},
],
},
windows: {
id: 'QuickLook.Plugin.Example',
extensions: ['example'],
},
framework: 'sveltekit',
});Each platform group is optional, but you need at least one: leave out windows to build for macOS only, or macos for Windows only.
A new type conforms to public.plain-text for text formats, or public.data for binary ones. To preview a type macOS already knows about, use a plain string like 'public.json' in place of an object.
The config can also be quicklook.config.js, .json, .yaml or .toml, or a .quicklookrc. The keys are the same in every format.
| Key | Default | |
| -------------- | ------------ | ---------------------------------------------------------------------------------- |
| name | required | App name, shown in Finder and System Settings |
| macos | none | macOS options, see below |
| windows | none | Windows options, see below |
| framework | required | 'sveltekit', 'astro', 'solid-start', 'nuxt', 'analog', or an integration |
| integrations | [] | See Integrations |
| csp | locked down | Directives here replace the defaults of the same name |
| maxFileSize | 10_000_000 | Larger files get Quick Look's generic preview |
| public | {} | Values the page can read at build time |
| settings | {} | Settings the user edits in the app, see Settings |
| devFile | none | A file the dev server previews until you drop another, relative to the project |
Under macos:
| Key | Default | |
| ---------------- | -------- | ----------------------------------------- |
| bundleId | required | The extension gets <bundleId>.Extension |
| types | required | File types to preview |
| icon | none | Path to an .icns file for the app |
| minimumVersion | '14.0' | Minimum macOS version |
Under windows:
| Key | Default | |
| ------------ | -------- | ---------------------------------------------------------------------------------- |
| id | required | Plugin name, starting with QuickLook.Plugin., e.g. QuickLook.Plugin.Example |
| extensions | required | File extensions to preview, without the dot. They win over QuickLook's own viewers |
Upgrading from a version without platform groups? The old top-level bundleId, icon, macOS and types now live under macos, with macOS renamed to minimumVersion.
A framework can take options as a [name, options] pair, e.g. ['sveltekit', { outDir: 'dist' }] when your adapter writes somewhere other than the default.
Each framework needs a static, client-side build:
| Framework | Requirement |
| ---------- | ------------------------------------------------------ |
| Analog | @analogjs/vite-plugin-angular |
| Astro | the default output: 'static' |
| Nuxt | ssr: false |
| SolidStart | ssr: false, Nitro's static preset prerendering / |
| SvelteKit | @sveltejs/adapter-static |
2. Add the Vite plugin
import { quicklook } from 'quicklook-kit/vite';
// in the framework's Vite config
plugins: [quicklook()];For TypeScript, add the types of the virtual module, either in a .d.ts file:
/// <reference types="quicklook-kit/virtual" />or in tsconfig.json under compilerOptions.types as "quicklook-kit/virtual".
3. Read the file
import { browserFile, config, nativeFile } from 'virtual:quicklook';
const file = nativeFile(); // the previewed file, undefined in the browser
config.public; // values from quicklook.config.*| Field | Type | |
| ----------- | ------------ | ------------------------------ |
| name | string | "notes.example" |
| extension | string | "example", lowercase, no dot |
| bytes | Uint8Array | the raw file contents |
Decode bytes however your format needs: new TextDecoder().decode(file.bytes) for text, or a parser for binary formats.
In the dev server, nativeFile() returns devFile if you set one, and undefined otherwise. Pass a dropped or picked File to browserFile() to get the same shape.
4. Add scripts
{
"scripts": {
"dev": "quicklook dev",
"build": "quicklook build",
"register": "quicklook build && quicklook register",
"unregister": "quicklook unregister"
}
}CLI
| Command | Description |
| ---------------------- | ----------------------------------------------------------------------------- |
| quicklook dev | Starts the framework's dev server |
| quicklook build:web | Builds the page only, into .quicklook/web |
| quicklook build | Builds every configured platform into .quicklook/build/<platform> |
| quicklook register | Installs the build on the current OS: the app on macOS, the plugin on Windows |
| quicklook unregister | Removes it again |
build takes --platform macos or --platform windows to build just one. The Windows plugin builds on any OS; the macOS app only on a Mac, so elsewhere build skips it. Run build and register again after every change.
On macOS, register copies the app to /Applications and registers its extension. Select a file in Finder and press Space. If the preview doesn't show up, check pluginkit -mAv | grep <your macos.bundleId>, and make sure no other app claims the same file type.
On Windows, register copies the plugin to %APPDATA%\pooi.moe\QuickLook\QuickLook.Plugin\<windows.id>. Quit and restart QuickLook, then select a file in Explorer and press Space.
Add .quicklook to your .gitignore.
Things to know
- Nothing can be loaded from the network. The native host serves your build from disk with a strict Content Security Policy, on both platforms. Import images, fonts and CSS normally and they get bundled. The dev server only reports violations in the console, without blocking them.
- Inline scripts are allowed by hash. Frameworks that bootstrap with inline
<script>tags keep working; the hashes are added to the CSP at build time. - Test in Safari and Edge. macOS previews run in WebKit, like Safari; Windows previews in WebView2, like Edge.
- The light/dark theme follows the system. Use
prefers-color-schemein CSS. - The macOS app is signed ad hoc. Other people have to allow it in System Settings → Privacy & Security on first launch. Distributing it without that step requires an Apple Developer account and notarization.
- Windows users install the
.qlpluginfrom.quicklook/build/windowsby previewing it in QuickLook and clicking the install button.
Releasing
Projects made with create quicklook include a release workflow. Push a tag like v1.0.0 and it builds every configured platform and attaches <App>-v1.0.0-macos.zip and <App>-v1.0.0-windows.qlplugin to a GitHub release.
If you publish a Homebrew cask, its URL needs the -macos suffix once you move to a release with platform groups.
Settings
settings declares options the user can change. On macOS, the app shows a control for each entry, picked by its type:
settings: {
theme: {
type: "string",
default: "light",
enum: [{ value: "light", description: "Light" }, "dark"],
radio: true,
},
wrap: { type: "boolean", default: true, description: "Wrap **long** lines" },
fontSize: { type: "integer", default: 13, minimum: 9, maximum: 32 },
editor: {
type: "object",
properties: { tabSize: { type: "integer", default: 2, enum: [2, 4, 8] } },
},
},| Type | Control |
| ------------------------------------------- | ------------------------------------------------------------------------- |
| boolean | Checkbox |
| enum (on string, integer or number) | Picker, or radio buttons with radio: true |
| string | Text field |
| integer, number | Number field, clamped to minimum/maximum; integer rejects fractions |
| object | A group with a heading, for nested properties |
Every entry except object needs a default. title defaults to the key in words (fontSize becomes "Font Size"), description is inline Markdown, and order sorts entries. enum entries are plain values or { value, description }. A schema with an unknown type or field, a field on the wrong type, a key containing ., or a default that doesn't fit its type, enum or bounds fails the build with the key path, e.g. settings.editor.tabSize: default 0 is below minimum 1.
The page reads the values, nested like the schema, with settings():
import { settings } from 'virtual:quicklook';
const { wrap, editor } = settings();In the dev server, settings() returns the defaults.
Windows has no settings window. Users set values in %APPDATA%\pooi.moe\QuickLook\<windows.id>.config, with nested keys joined by dots, and restart QuickLook. Values that don't fit the schema fall back to the default.
<?xml version="1.0" encoding="utf-8"?>
<Settings>
<theme>dark</theme>
<editor.tabSize>4</editor.tabSize>
</Settings>Integrations
An integration is an object with a name and any of these hooks. They run in order: the framework first, then each entry in integrations.
import type { Integration } from 'quicklook-kit';
export default (options: { key: string }): Integration => ({
name: 'my-integration',
hooks: {
// Adjust the resolved config.
'config:resolved'(config) {},
// Post-process the framework's static output before it's bundled.
async 'build:done'({ outDir, config }) {},
// Edit the generated XcodeGen spec: Info.plist keys, entitlements, targets. macOS only.
'xcode:spec'(spec, config) {
spec.targets.QuickLookExampleExtension.info.properties.MyKey = options.key;
},
// Add Vite plugins wherever quicklook() is used.
vite: () => [],
},
});Add it as an object, or by name so JSON and YAML configs can use it too. A name is imported from the project: a package or a relative path, whose default export is the integration or a function returning it.
{
"integrations": ["quicklook-some-package", ["./my-integration.ts", { "key": "value" }]]
}A framework is an integration with a build field:
framework: {
name: 'my-framework',
build: {
dev: ['vite', 'dev'],
command: ['vite', 'build'],
outDir: 'dist'
}
}The commands run with the project's own binaries.
License
This work is licensed under The MIT License.
