Quick Look preview and Spotlight import extensions !104

merged merged by cmc on 2026-10-06 07:03 UTC · krz/orgstar:finder-extensions into main

8 files changed, +145 −1

Layout: unified · split

Package.swift +9 −1
@@ -1,6 +1,9 @@
11// swift-tools-version: 6.2
22import PackageDescription
33
4/// App extensions start in Foundation's NSExtensionMain.
5let extensionEntry: [LinkerSetting] = [.unsafeFlags(["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"])]
6
47let package = Package(
58 name: "Orgstar",
69 platforms: [.macOS(.v26), .iOS(.v26)],
@@ -14,7 +17,9 @@ let package = Package(
1417 .executable(name: "OrgstarSpike", targets: ["OrgstarSpike"]),
1518 .library(name: "OrgApp", targets: ["OrgApp"]),
1619 .library(name: "OrgHighlight", targets: ["OrgHighlight"]),
17 .executable(name: "Orgstar", targets: ["Orgstar"])
20 .executable(name: "Orgstar", targets: ["Orgstar"]),
21 .executable(name: "OrgstarQuickLook", targets: ["OrgstarQuickLook"]),
22 .executable(name: "OrgstarSpotlight", targets: ["OrgstarSpotlight"])
1823 ],
1924 dependencies: [
2025 .package(url: "https://github.com/groue/GRDB.swift", from: "7.11.1"),
@@ -75,6 +80,9 @@ let package = Package(
7580 .executableTarget(name: "OrgstarSpike", dependencies: ["OrgEditorAppKit"]),
7681 .target(name: "OrgApp", dependencies: ["OrgWorkspace", "OrgDocument", "OrgPresentation"]),
7782 .executableTarget(name: "Orgstar", dependencies: ["OrgApp", "OrgEditorAppKit", "OrgHighlight"]),
83 // App extensions, bundled into Orgstar.app by scripts/build-app.sh.
84 .executableTarget(name: "OrgstarQuickLook", dependencies: ["OrgCore"], linkerSettings: extensionEntry),
85 .executableTarget(name: "OrgstarSpotlight", dependencies: ["OrgCore"], linkerSettings: extensionEntry),
7886 .testTarget(name: "OrgCoreTests", dependencies: ["OrgCore"]),
7987 .testTarget(name: "OrgDocumentTests", dependencies: ["OrgDocument"]),
8088 .testTarget(name: "OrgIndexTests", dependencies: ["OrgIndex"]),
Sources/OrgCore/Semantic/FileSummary.swift added +32
@@ -0,0 +1,32 @@
1import Foundation
2
3/// What Spotlight shows and searches for a file: its keywords, tags and headings.
4public struct FileSummary: Sendable, Equatable {
5 public var title: String?
6 public var author: String?
7 public var description: String?
8 /// `#+FILETAGS` and every heading's tags, in order, once each.
9 public var tags: [String]
10 /// Heading titles, with TODO keywords, priorities and tags left out.
11 public var headings: [String]
12
13 public init(_ text: String) {
14 let tree = OrgParser.parse(text)
15 func keyword(_ name: String) -> String? {
16 let pattern = "^[ \\t]*#\\+" + name + ":[ \\t]*(.*?)[ \\t]*$"
17 guard let m = try! NSRegularExpression(pattern: pattern, options: [.anchorsMatchLines, .caseInsensitive])
18 .firstMatch(in: text, range: NSRange(location: 0, length: (text as NSString).length)) else { return nil }
19 let value = (text as NSString).substring(with: m.range(at: 1))
20 return value.isEmpty ? nil : value
21 }
22 title = keyword("TITLE")
23 author = keyword("AUTHOR")
24 description = keyword("DESCRIPTION")
25 let model = DocumentModel(tree: tree)
26 var tags = (keyword("FILETAGS") ?? "").split { $0 == ":" || $0 == " " }.map(String.init)
27 for heading in model.headings { tags += heading.localTags }
28 var seen = Set<String>()
29 self.tags = tags.filter { seen.insert($0).inserted }
30 headings = model.headings.map { Links.normalizedTitle($0.title) }
31 }
32}
Sources/OrgstarQuickLook/PreviewProvider.swift added +14
@@ -0,0 +1,14 @@
1import Foundation
2import OrgCore
3import QuickLookUI
4
5/// Quick Look's preview of an org file: the file as the HTML export renders it.
6@objc(PreviewProvider)
7final class PreviewProvider: QLPreviewProvider, QLPreviewingController {
8 func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply {
9 let text = try String(contentsOf: request.fileURL, encoding: .utf8)
10 let title = request.fileURL.deletingPathExtension().lastPathComponent
11 let html = HTMLExport.page(text, fallbackTitle: title, path: request.fileURL.path)
12 return QLPreviewReply(dataOfContentType: .html, contentSize: CGSize(width: 800, height: 800)) { _ in Data(html.utf8) }
13 }
14}
Sources/OrgstarQuickLook/main.swift added +2
@@ -0,0 +1,2 @@
1// The extension starts in NSExtensionMain (linker entry point); this file only makes the
2// target an executable.
Sources/OrgstarSpotlight/ImportExtension.swift added +17
@@ -0,0 +1,17 @@
1import CoreSpotlight
2import Foundation
3import OrgCore
4
5/// Spotlight's importer for org files: the text, plus the title, author, tags and headings.
6@objc(ImportExtension)
7final class ImportExtension: CSImportExtension {
8 override func update(_ attributes: CSSearchableItemAttributeSet, forFileAt contentURL: URL) throws {
9 let text = try String(contentsOf: contentURL, encoding: .utf8)
10 let summary = FileSummary(text)
11 attributes.textContent = text
12 attributes.title = summary.title ?? contentURL.deletingPathExtension().lastPathComponent
13 if let author = summary.author { attributes.authorNames = [author] }
14 if let description = summary.description { attributes.contentDescription = description }
15 attributes.keywords = summary.tags + summary.headings
16 }
17}
Sources/OrgstarSpotlight/main.swift added +2
@@ -0,0 +1,2 @@
1// The extension starts in NSExtensionMain (linker entry point); this file only makes the
2// target an executable.
Tests/OrgCoreTests/FileSummaryTests.swift added +12
@@ -0,0 +1,12 @@
1import Testing
2@testable import OrgCore
3
4struct FileSummaryTests {
5 @Test func readsKeywordsTagsAndHeadings() {
6 let summary = FileSummary("#+title: Notes\n#+AUTHOR: Me\n#+FILETAGS: :home:\n* TODO [#A] Fix the sink [1/2] :home:diy:\n** Buy parts\n")
7 #expect(summary.title == "Notes" && summary.author == "Me" && summary.description == nil)
8 #expect(summary.tags == ["home", "diy"])
9 #expect(summary.headings == ["Fix the sink", "Buy parts"])
10 #expect(FileSummary("plain\n").title == nil)
11 }
12}
scripts/build-app.sh +57
@@ -9,6 +9,8 @@ version="${1:-0.1.0}"
99app=".build/app/Orgstar.app"
1010
1111swift build -c release --product Orgstar
12swift build -c release --product OrgstarQuickLook
13swift build -c release --product OrgstarSpotlight
1214binary="$(swift build -c release --show-bin-path)/Orgstar"
1315
1416rm -rf "$app"
@@ -113,5 +115,60 @@ echo "$PWD/$objects/Orgstar-primary.swiftconstvalues" > "$work/constvalues.txt"
113115 --swift-const-vals-list "$work/constvalues.txt" --output "$app/Contents/Resources" --force > /dev/null 2>&1
114116rm -rf "$work"
115117
118# App extensions: Quick Look previews and the Spotlight importer. They run sandboxed.
119appex() { # name, extension point, principal class, attributes
120 dir="$app/Contents/PlugIns/$1.appex/Contents"
121 mkdir -p "$dir/MacOS"
122 cp "$(dirname "$binary")/$1" "$dir/MacOS/$1"
123 cat > "$dir/Info.plist" <<PLIST
124<?xml version="1.0" encoding="UTF-8"?>
125<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
126<plist version="1.0">
127<dict>
128 <key>CFBundleIdentifier</key>
129 <string>sh.krz.orgstar.$(echo "$1" | sed 's/^Orgstar//' | tr 'A-Z' 'a-z')</string>
130 <key>CFBundleName</key>
131 <string>$1</string>
132 <key>CFBundleExecutable</key>
133 <string>$1</string>
134 <key>CFBundlePackageType</key>
135 <string>XPC!</string>
136 <key>CFBundleShortVersionString</key>
137 <string>${version}</string>
138 <key>CFBundleVersion</key>
139 <string>${version}</string>
140 <key>LSMinimumSystemVersion</key>
141 <string>26.0</string>
142 <key>NSExtension</key>
143 <dict>
144 <key>NSExtensionPointIdentifier</key>
145 <string>$2</string>
146 <key>NSExtensionPrincipalClass</key>
147 <string>$3</string>
148 <key>NSExtensionAttributes</key>
149 <dict>$4</dict>
150 </dict>
151</dict>
152</plist>
153PLIST
154 cat > "$work/sandbox.entitlements" <<PLIST
155<?xml version="1.0" encoding="UTF-8"?>
156<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
157<plist version="1.0">
158<dict>
159 <key>com.apple.security.app-sandbox</key>
160 <true/>
161</dict>
162</plist>
163PLIST
164 codesign --force --sign - --entitlements "$work/sandbox.entitlements" "$app/Contents/PlugIns/$1.appex"
165}
166work="$(mktemp -d)"
167appex OrgstarQuickLook com.apple.quicklook.preview PreviewProvider \
168 "<key>QLSupportedContentTypes</key><array><string>org.orgmode.org</string></array><key>QLIsDataBasedPreview</key><true/>"
169appex OrgstarSpotlight com.apple.spotlight.import ImportExtension \
170 "<key>CSSupportedContentTypes</key><array><string>org.orgmode.org</string></array>"
171rm -rf "$work"
172
116173codesign --force --sign - "$app"
117174echo "$app"