Commit 2081575ac0
Verified · cmc
Layout: unified · split
Package.swift +9 −1
| @@ -1,6 +1,9 @@ | |||
| 1 | // swift-tools-version: 6.2 | 1 | // swift-tools-version: 6.2 |
| 2 | import PackageDescription | 2 | import PackageDescription |
| 3 | 3 | ||
| 4 | /// App extensions start in Foundation's NSExtensionMain. | ||
| 5 | let extensionEntry: [LinkerSetting] = [.unsafeFlags(["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"])] | ||
| 6 | |||
| 4 | let package = Package( | 7 | let package = Package( |
| 5 | name: "Orgstar", | 8 | name: "Orgstar", |
| 6 | platforms: [.macOS(.v26), .iOS(.v26)], | 9 | platforms: [.macOS(.v26), .iOS(.v26)], |
| @@ -14,7 +17,9 @@ let package = Package( | |||
| 14 | .executable(name: "OrgstarSpike", targets: ["OrgstarSpike"]), | 17 | .executable(name: "OrgstarSpike", targets: ["OrgstarSpike"]), |
| 15 | .library(name: "OrgApp", targets: ["OrgApp"]), | 18 | .library(name: "OrgApp", targets: ["OrgApp"]), |
| 16 | .library(name: "OrgHighlight", targets: ["OrgHighlight"]), | 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 | dependencies: [ | 24 | dependencies: [ |
| 20 | .package(url: "https://github.com/groue/GRDB.swift", from: "7.11.1"), | 25 | .package(url: "https://github.com/groue/GRDB.swift", from: "7.11.1"), |
| @@ -75,6 +80,9 @@ let package = Package( | |||
| 75 | .executableTarget(name: "OrgstarSpike", dependencies: ["OrgEditorAppKit"]), | 80 | .executableTarget(name: "OrgstarSpike", dependencies: ["OrgEditorAppKit"]), |
| 76 | .target(name: "OrgApp", dependencies: ["OrgWorkspace", "OrgDocument", "OrgPresentation"]), | 81 | .target(name: "OrgApp", dependencies: ["OrgWorkspace", "OrgDocument", "OrgPresentation"]), |
| 77 | .executableTarget(name: "Orgstar", dependencies: ["OrgApp", "OrgEditorAppKit", "OrgHighlight"]), | 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 | .testTarget(name: "OrgCoreTests", dependencies: ["OrgCore"]), | 86 | .testTarget(name: "OrgCoreTests", dependencies: ["OrgCore"]), |
| 79 | .testTarget(name: "OrgDocumentTests", dependencies: ["OrgDocument"]), | 87 | .testTarget(name: "OrgDocumentTests", dependencies: ["OrgDocument"]), |
| 80 | .testTarget(name: "OrgIndexTests", dependencies: ["OrgIndex"]), | 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 | app=".build/app/Orgstar.app" | 9 | app=".build/app/Orgstar.app" |
| 10 | 10 | ||
| 11 | swift build -c release --product Orgstar | 11 | swift build -c release --product Orgstar |
| 12 | swift build -c release --product OrgstarQuickLook | ||
| 13 | swift build -c release --product OrgstarSpotlight | ||
| 12 | binary="$(swift build -c release --show-bin-path)/Orgstar" | 14 | binary="$(swift build -c release --show-bin-path)/Orgstar" |
| 13 | 15 | ||
| 14 | rm -rf "$app" | 16 | rm -rf "$app" |
| @@ -113,5 +115,60 @@ echo "$PWD/$objects/Orgstar-primary.swiftconstvalues" > "$work/constvalues.txt" | |||
| 113 | --swift-const-vals-list "$work/constvalues.txt" --output "$app/Contents/Resources" --force > /dev/null 2>&1 | 115 | --swift-const-vals-list "$work/constvalues.txt" --output "$app/Contents/Resources" --force > /dev/null 2>&1 |
| 114 | rm -rf "$work" | 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 | codesign --force --sign - "$app" | 173 | codesign --force --sign - "$app" |
| 117 | echo "$app" | 174 | echo "$app" |