quicklook-kit 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,3 +1,7 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jan T. Sott
4
+
1
5
  Permission is hereby granted, free of charge, to any
2
6
  person obtaining a copy of this software and associated
3
7
  documentation files (the "Software"), to deal in the
package/README.markdown CHANGED
@@ -4,9 +4,9 @@
4
4
  [![Version](https://img.shields.io/npm/v/quicklook-kit?style=for-the-badge)](https://www.npmjs.org/package/quicklook-kit)
5
5
  [![Build](https://img.shields.io/github/actions/workflow/status/idleberg/quicklook-kit/ci.yml?style=for-the-badge)](https://github.com/idleberg/quicklook-kit/actions)
6
6
 
7
- > Build macOS QuickLook previews with any Vite-based web framework, no Swift required
7
+ > Build macOS Quick Look previews with any Vite-based web framework, no Swift required
8
8
 
9
- Your preview is a web page. quicklook-kit builds it with your framework, wraps it in a small native QuickLook extension and installs the resulting app. You never touch Swift or Xcode.
9
+ Your preview is a web page. quicklook-kit builds it with your framework, wraps it in a small native Quick Look extension and installs the resulting app. You never touch Swift or Xcode.
10
10
 
11
11
  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.
12
12
 
@@ -34,11 +34,11 @@ Create `quicklook.config.ts` in your project root:
34
34
  import { defineConfig } from "quicklook-kit";
35
35
 
36
36
  export default defineConfig({
37
- name: "Example Preview",
38
- bundleId: "com.example.ExamplePreview",
37
+ name: "Quick Look Example",
38
+ bundleId: "com.example.QuickLookExample",
39
39
  types: [
40
40
  {
41
- id: "com.example.ExamplePreview.example",
41
+ id: "com.example.QuickLookExample.example",
42
42
  description: "Example File",
43
43
  extensions: ["example"],
44
44
  conformsTo: ["public.plain-text"],
@@ -62,7 +62,7 @@ The config can also be `quicklook.config.js`, `.json`, `.yaml` or `.toml`, or a
62
62
  | `macOS` | `'14.0'` | Minimum macOS version |
63
63
  | `integrations` | `[]` | See [Integrations](#integrations) |
64
64
  | `csp` | locked down | Directives here replace the defaults of the same name |
65
- | `maxFileSize` | `10_000_000` | Larger files get QuickLook's generic preview |
65
+ | `maxFileSize` | `10_000_000` | Larger files get Quick Look's generic preview |
66
66
  | `public` | `{}` | Values the page can read at build time |
67
67
 
68
68
  A framework can take options as a `[name, options]` pair, e.g. `['sveltekit', { outDir: 'dist' }]` when your adapter writes somewhere other than the default.
@@ -99,7 +99,7 @@ or in `tsconfig.json` under `compilerOptions.types` as `"quicklook-kit/virtual"`
99
99
  ```ts
100
100
  import { browserFile, config, nativeFile } from "virtual:quicklook";
101
101
 
102
- const file = nativeFile(); // the previewed file inside QuickLook, undefined in the browser
102
+ const file = nativeFile(); // the previewed file inside Quick Look, undefined in the browser
103
103
  config.public; // values from quicklook.config.*
104
104
  ```
105
105
 
@@ -146,7 +146,7 @@ Add `.quicklook` to your `.gitignore`.
146
146
 
147
147
  - **Nothing can be loaded from the network.** The extension serves your build from inside the app with a strict Content Security Policy. Import images, fonts and CSS normally and they get bundled. The dev server only reports violations in the console, without blocking them.
148
148
  - **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.
149
- - **Test in Safari.** It uses the same WebKit engine as QuickLook's WKWebView.
149
+ - **Test in Safari.** It uses the same WebKit engine as Quick Look's WKWebView.
150
150
  - **The light/dark theme follows the system.** Use `prefers-color-scheme` in CSS.
151
151
  - **The 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.
152
152
 
@@ -168,7 +168,7 @@ export default (options: { key: string }): Integration => ({
168
168
 
169
169
  // Edit the generated XcodeGen spec: Info.plist keys, entitlements, targets.
170
170
  "xcode:spec"(spec, config) {
171
- spec.targets.ExamplePreviewExtension.info.properties.MyKey = options.key;
171
+ spec.targets.QuickLookExampleExtension.info.properties.MyKey = options.key;
172
172
  },
173
173
 
174
174
  // Add Vite plugins wherever quicklook() is used.
@@ -190,7 +190,11 @@ A framework is an integration with a `build` field:
190
190
  ```ts
191
191
  framework: {
192
192
  name: 'my-framework',
193
- build: { dev: ['vite', 'dev'], command: ['vite', 'build'], outDir: 'dist' }
193
+ build: {
194
+ dev: ['vite', 'dev'],
195
+ command: ['vite', 'build'],
196
+ outDir: 'dist'
197
+ }
194
198
  }
195
199
  ```
196
200
 
package/dist/config.d.mts CHANGED
@@ -55,7 +55,7 @@ export interface QuickLookConfig {
55
55
  integrations?: IntegrationRef[];
56
56
  /** Directives given here replace the defaults of the same name. */
57
57
  csp?: Csp;
58
- /** Larger files get QuickLook's generic preview. Defaults to 10 MB. */
58
+ /** Larger files get Quick Look's generic preview. Defaults to 10 MB. */
59
59
  maxFileSize?: number;
60
60
  /** Exposed to the page as `config.public` from `virtual:quicklook`. */
61
61
  public?: Record<string, unknown>;
@@ -1,5 +1,5 @@
1
1
  //#region src/runtime.d.ts
2
- /** The file QuickLook asked to preview. */
2
+ /** The file Quick Look asked to preview. */
3
3
  export interface QuickLookFile {
4
4
  name: string;
5
5
  /** Lowercase, without the dot. */
@@ -16,7 +16,7 @@ declare global {
16
16
  };
17
17
  }
18
18
  }
19
- /** The previewed file inside QuickLook; `undefined` in the browser. */
19
+ /** The previewed file inside Quick Look; `undefined` in the browser. */
20
20
  export declare function nativeFile(): QuickLookFile | undefined;
21
21
  /** The same shape from a file picked or dropped in the browser, for the dev server. */
22
22
  export declare function browserFile(file: File): Promise<QuickLookFile>;
package/dist/runtime.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/runtime.ts
2
- /** The previewed file inside QuickLook; `undefined` in the browser. */
2
+ /** The previewed file inside Quick Look; `undefined` in the browser. */
3
3
  function nativeFile() {
4
4
  const injected = window.quicklook;
5
5
  if (!injected) return;
@@ -1,21 +1,170 @@
1
1
  import SwiftUI
2
+ import UniformTypeIdentifiers
2
3
 
3
- /// QuickLook extensions have to ship inside an app, and macOS registers them
4
- /// when that app is first launched. This is all the app does.
4
+ /// Quick Look extensions have to ship inside an app, and macOS registers them
5
+ /// when that app is first launched. Beyond that, the app only lets the user
6
+ /// enable or disable the extension.
5
7
  @main
6
8
  struct HostApp: App {
7
9
  private let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? ""
8
10
 
9
11
  var body: some Scene {
10
12
  Window(name, id: "main") {
13
+ ContentView(name: name)
14
+ .padding(32)
15
+ .fixedSize()
16
+ }
17
+ .windowResizability(.contentSize)
18
+ }
19
+ }
20
+
21
+ private struct ContentView: View {
22
+ let name: String
23
+
24
+ // Matches the extension's PRODUCT_BUNDLE_IDENTIFIER in cli.ts.
25
+ private let extensionId = "\(Bundle.main.bundleIdentifier ?? "").Extension"
26
+
27
+ @State private var isEnabled = true
28
+ @State private var status: String?
29
+ @State private var conflicts: [String] = []
30
+
31
+ var body: some View {
32
+ VStack(spacing: 16) {
11
33
  VStack(spacing: 8) {
12
34
  Text(name).font(.headline)
13
- Text("The QuickLook preview is installed. You can close this window.")
35
+ Text("Previews supported files when you press Space in Finder.")
14
36
  .foregroundStyle(.secondary)
37
+ .multilineTextAlignment(.center)
15
38
  }
16
- .padding(32)
17
- .fixedSize()
39
+
40
+ Button(isEnabled ? "Disable Preview" : "Enable Preview", action: toggle)
41
+
42
+ if let status {
43
+ Text(status).font(.caption).foregroundStyle(.secondary)
44
+ }
45
+
46
+ if isEnabled && !conflicts.isEmpty {
47
+ VStack(alignment: .leading, spacing: 8) {
48
+ Text("⚠️ \(conflicts.count == 1 ? "There is a conflict:" : "There are conflicts:")")
49
+ .bold()
50
+ ForEach(conflicts, id: \.self) { conflict in
51
+ Label {
52
+ Text(conflict)
53
+ } icon: {
54
+ Text("•")
55
+ }
56
+ .fixedSize(horizontal: false, vertical: true)
57
+ }
58
+ }
59
+ .font(.callout)
60
+ .frame(maxWidth: .infinity, alignment: .leading)
61
+ }
62
+
63
+ Divider()
64
+
65
+ Text("Made with [quicklook-kit](https://github.com/idleberg/quicklook-kit)")
66
+ .font(.caption)
67
+ .foregroundStyle(.tertiary)
68
+ .tint(.secondary)
18
69
  }
19
- .windowResizability(.contentSize)
70
+ // Fixed so the window only grows in height as text comes and goes.
71
+ .frame(width: 360)
72
+ .onAppear(perform: refresh)
73
+ }
74
+
75
+ private func refresh() {
76
+ isEnabled = currentlyEnabled()
77
+ // The same app installed twice yields the same line twice; ForEach needs unique ids.
78
+ var seen = Set<String>()
79
+ conflicts = findConflicts().filter { seen.insert($0).inserted }
80
+ }
81
+
82
+ /// Other apps that would win over this preview for some of its file types.
83
+ /// ponytail: how Quick Look picks between conflicting extensions isn't documented, so
84
+ /// this is a best guess. Any overlap is reported, even if we'd happen to win.
85
+ private func findConflicts() -> [String] {
86
+ var conflicts: [String] = []
87
+
88
+ // Same file extension, two types: LaunchServices resolves the extension to only one.
89
+ // UTIs are case-insensitive, and LaunchServices returns them lowercased.
90
+ let declarations = Bundle.main.object(forInfoDictionaryKey: "UTExportedTypeDeclarations")
91
+ for declaration in declarations as? [[String: Any]] ?? [] {
92
+ guard let id = declaration["UTTypeIdentifier"] as? String,
93
+ let tags = declaration["UTTypeTagSpecification"] as? [String: Any],
94
+ let extensions = tags["public.filename-extension"] as? [String]
95
+ else { continue }
96
+ for ext in extensions {
97
+ guard let other = UTType(filenameExtension: ext)?.identifier,
98
+ other.lowercased() != id.lowercased()
99
+ else { continue }
100
+ conflicts.append("“.\(ext)” files are claimed by another app’s type “\(other)”.")
101
+ }
102
+ }
103
+
104
+ // Same type, two extensions.
105
+ let ourTypes = supportedTypes(Bundle.main.builtInPlugInsURL?.appendingPathComponent(appexName))
106
+ guard let output = pluginkit("-m", "-v", "-p", "com.apple.quicklook.preview") else {
107
+ return conflicts
108
+ }
109
+ for line in output.split(separator: "\n") {
110
+ guard let path = line.split(separator: "\t").last.map(String.init),
111
+ !line.trimmingCharacters(in: .whitespaces).hasPrefix("-"),
112
+ !line.contains(" \(extensionId)("),
113
+ !path.hasPrefix("/System/")
114
+ else { continue }
115
+ let theirTypes = supportedTypes(URL(fileURLWithPath: path))
116
+ let overlaps = ourTypes.contains { ours in theirTypes.contains { ours.conforms(to: $0) } }
117
+ guard overlaps else { continue }
118
+ let app = path.split(separator: "/").first { $0.hasSuffix(".app") }?.dropLast(4)
119
+ conflicts.append(
120
+ "“\(app ?? Substring(path))” also previews these files. Turn it off in System Settings → "
121
+ + "Login Items & Extensions → Quick Look.")
122
+ }
123
+ return conflicts
124
+ }
125
+
126
+ private var appexName: String {
127
+ let plugins = Bundle.main.builtInPlugInsPath ?? ""
128
+ let items = (try? FileManager.default.contentsOfDirectory(atPath: plugins)) ?? []
129
+ return items.first { $0.hasSuffix(".appex") } ?? ""
130
+ }
131
+
132
+ private func supportedTypes(_ appex: URL?) -> [UTType] {
133
+ guard let appex, let info = Bundle(url: appex)?.infoDictionary,
134
+ let attributes = (info["NSExtension"] as? [String: Any])?["NSExtensionAttributes"]
135
+ as? [String: Any],
136
+ let ids = attributes["QLSupportedContentTypes"] as? [String]
137
+ else { return [] }
138
+ return ids.compactMap(UTType.init)
139
+ }
140
+
141
+ /// `pluginkit -mv` prefixes a line with `-` when the extension is ignored,
142
+ /// `+` when explicitly enabled, and nothing when enabled by default.
143
+ private func currentlyEnabled() -> Bool {
144
+ guard let output = pluginkit("-m", "-v", "-i", extensionId) else { return true }
145
+ return !output.trimmingCharacters(in: .whitespaces).hasPrefix("-")
146
+ }
147
+
148
+ private func toggle() {
149
+ guard pluginkit("-e", isEnabled ? "ignore" : "use", "-i", extensionId) != nil else {
150
+ status = "Couldn't change the preview. Try again from System Settings."
151
+ return
152
+ }
153
+ refresh()
154
+ status = isEnabled ? "Preview enabled." : "Preview disabled."
155
+ }
156
+
157
+ /// Returns stdout, or nil if pluginkit failed to run or exited non-zero.
158
+ private func pluginkit(_ args: String...) -> String? {
159
+ let process = Process()
160
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/pluginkit")
161
+ process.arguments = args
162
+ let pipe = Pipe()
163
+ process.standardOutput = pipe
164
+ guard (try? process.run()) != nil else { return nil }
165
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
166
+ process.waitUntilExit()
167
+ guard process.terminationStatus == 0 else { return nil }
168
+ return String(data: data, encoding: .utf8)
20
169
  }
21
170
  }
@@ -6,7 +6,7 @@ import WebKit
6
6
  /// file as `window.quicklook`. See packages/quicklook-kit/src/runtime.ts.
7
7
  @objc(PreviewViewController)
8
8
  final class PreviewViewController: NSViewController, QLPreviewingController {
9
- /// Larger files throw, which makes QuickLook fall back to its generic preview
9
+ /// Larger files throw, which makes Quick Look fall back to its generic preview
10
10
  /// instead of stalling while megabytes of base64 cross into the page.
11
11
  private static let maxFileSize =
12
12
  Bundle.main.object(forInfoDictionaryKey: "QLMaxFileSize") as? Int ?? 10_000_000
@@ -43,7 +43,7 @@ final class PreviewViewController: NSViewController, QLPreviewingController {
43
43
  source: "window.quicklook = \(json);", injectionTime: .atDocumentStart,
44
44
  forMainFrameOnly: true))
45
45
 
46
- // QuickLook displays the view as soon as this returns, so wait for the
46
+ // Quick Look displays the view as soon as this returns, so wait for the
47
47
  // page to actually finish loading — otherwise larger files show blank.
48
48
  let page = URLRequest(url: URL(string: "quicklook://app/")!)
49
49
  try await loadWaiter.load { webView.load(page) }
@@ -117,7 +117,7 @@ private final class LoadWaiter: NSObject, WKNavigationDelegate {
117
117
  }
118
118
 
119
119
  /// A crashed content process fires no navigation callback at all — without
120
- /// this the await never returns and QuickLook spins forever.
120
+ /// this the await never returns and Quick Look spins forever.
121
121
  func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
122
122
  finish(.failure(CocoaError(.featureUnsupported)))
123
123
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "quicklook-kit",
3
- "version": "0.3.1",
4
- "description": "Build macOS QuickLook previews with any Vite-based web framework",
3
+ "version": "0.5.0",
4
+ "description": "Build macOS Quick Look previews with any Vite-based web framework",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "quicklook",
@@ -51,9 +51,9 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "24.19.2",
54
- "tsdown": "0.23.0",
54
+ "tsdown": "0.23.1",
55
55
  "typescript": "6.0.3",
56
- "vite": "8.3.0"
56
+ "vite": "8.3.4"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "vite": ">=7"