Commit 2081575ac0
2081575ac057adc9e191c302b52e5dc4fa0b120c
parent: 30f6716c5b
Verified · cmc
cmc <hello@cleberg.net> · 2026-10-06 07:03 UTC
Add Quick Look preview and Spotlight import extensions
OrgstarQuickLook renders .org files through the HTML export as a data-based
preview. OrgstarSpotlight indexes the text with the #+TITLE, #+AUTHOR,
#+DESCRIPTION, tags and heading titles. Both are SwiftPM executables linked
against NSExtensionMain; build-app.sh bundles them sandboxed into PlugIns.
Ref docs/review-2026-10-05.md tier 3 item 3
Layout: unified · split
Package.swift
+9 −1
| @@ -1,6 +1,9 @@ |
| 1 | 1 | // swift-tools-version: 6.2 |
| 2 | 2 | import PackageDescription |
| 3 | 3 | |
| 4 | /// App extensions start in Foundation's NSExtensionMain. |
| 5 | let extensionEntry: [LinkerSetting] = [.unsafeFlags(["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"])] |
| 6 | |
| 4 | 7 | let package = Package( |
| 5 | 8 | name: "Orgstar", |
| 6 | 9 | platforms: [.macOS(.v26), .iOS(.v26)], |
| @@ -14,7 +17,9 @@ let package = Package( |
| 14 | 17 | .executable(name: "OrgstarSpike", targets: ["OrgstarSpike"]), |
| 15 | 18 | .library(name: "OrgApp", targets: ["OrgApp"]), |
| 16 | 19 | .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"]) |
| 18 | 23 | ], |
| 19 | 24 | dependencies: [ |
| 20 | 25 | .package(url: "https://github.com/groue/GRDB.swift", from: "7.11.1"), |
| @@ -75,6 +80,9 @@ let package = Package( |
| 75 | 80 | .executableTarget(name: "OrgstarSpike", dependencies: ["OrgEditorAppKit"]), |
| 76 | 81 | .target(name: "OrgApp", dependencies: ["OrgWorkspace", "OrgDocument", "OrgPresentation"]), |
| 77 | 82 | .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), |
| 78 | 86 | .testTarget(name: "OrgCoreTests", dependencies: ["OrgCore"]), |
| 79 | 87 | .testTarget(name: "OrgDocumentTests", dependencies: ["OrgDocument"]), |
| 80 | 88 | .testTarget(name: "OrgIndexTests", dependencies: ["OrgIndex"]), |
Sources/OrgCore/Semantic/FileSummary.swift
added
+32
| @@ -0,0 +1,32 @@ |
| 1 | import Foundation |
| 2 | |
| 3 | /// What Spotlight shows and searches for a file: its keywords, tags and headings. |
| 4 | public 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 @@ |
| 1 | import Foundation |
| 2 | import OrgCore |
| 3 | import QuickLookUI |
| 4 | |
| 5 | /// Quick Look's preview of an org file: the file as the HTML export renders it. |
| 6 | @objc(PreviewProvider) |
| 7 | final 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 @@ |
| 1 | import CoreSpotlight |
| 2 | import Foundation |
| 3 | import OrgCore |
| 4 | |
| 5 | /// Spotlight's importer for org files: the text, plus the title, author, tags and headings. |
| 6 | @objc(ImportExtension) |
| 7 | final 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 @@ |
| 1 | import Testing |
| 2 | @testable import OrgCore |
| 3 | |
| 4 | struct 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}" |
| 9 | 9 | app=".build/app/Orgstar.app" |
| 10 | 10 | |
| 11 | 11 | swift build -c release --product Orgstar |
| 12 | swift build -c release --product OrgstarQuickLook |
| 13 | swift build -c release --product OrgstarSpotlight |
| 12 | 14 | binary="$(swift build -c release --show-bin-path)/Orgstar" |
| 13 | 15 | |
| 14 | 16 | rm -rf "$app" |
| @@ -113,5 +115,60 @@ echo "$PWD/$objects/Orgstar-primary.swiftconstvalues" > "$work/constvalues.txt" |
| 113 | 115 | --swift-const-vals-list "$work/constvalues.txt" --output "$app/Contents/Resources" --force > /dev/null 2>&1 |
| 114 | 116 | rm -rf "$work" |
| 115 | 117 | |
| 118 | # App extensions: Quick Look previews and the Spotlight importer. They run sandboxed. |
| 119 | appex() { # 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> |
| 153 | PLIST |
| 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> |
| 163 | PLIST |
| 164 | codesign --force --sign - --entitlements "$work/sandbox.entitlements" "$app/Contents/PlugIns/$1.appex" |
| 165 | } |
| 166 | work="$(mktemp -d)" |
| 167 | appex OrgstarQuickLook com.apple.quicklook.preview PreviewProvider \ |
| 168 | "<key>QLSupportedContentTypes</key><array><string>org.orgmode.org</string></array><key>QLIsDataBasedPreview</key><true/>" |
| 169 | appex OrgstarSpotlight com.apple.spotlight.import ImportExtension \ |
| 170 | "<key>CSSupportedContentTypes</key><array><string>org.orgmode.org</string></array>" |
| 171 | rm -rf "$work" |
| 172 | |
| 116 | 173 | codesign --force --sign - "$app" |
| 117 | 174 | echo "$app" |