krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 6eb1bcc847

6eb1bcc847488736724335ea55de55be1816a3aa

parent: 53903567a6

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-05 00:34 UTC

Add spike app

Layout: unified · split

Package.swift +3 −1
@@ -10,7 +10,8 @@ let package = Package(
1010 .library(name: "OrgIndex", targets: ["OrgIndex"]),
1111 .library(name: "OrgWorkspace", targets: ["OrgWorkspace"]),
1212 .library(name: "OrgPresentation", targets: ["OrgPresentation"]),
13 .library(name: "OrgEditorAppKit", targets: ["OrgEditorAppKit"])
13 .library(name: "OrgEditorAppKit", targets: ["OrgEditorAppKit"]),
14 .executable(name: "OrgstarSpike", targets: ["OrgstarSpike"])
1415 ],
1516 dependencies: [
1617 .package(url: "https://github.com/groue/GRDB.swift", from: "7.11.1")
@@ -22,6 +23,7 @@ let package = Package(
2223 .target(name: "OrgWorkspace", dependencies: ["OrgIndex"]),
2324 .target(name: "OrgPresentation", dependencies: ["OrgCore"]),
2425 .target(name: "OrgEditorAppKit", dependencies: ["OrgDocument", "OrgPresentation"]),
26 .executableTarget(name: "OrgstarSpike", dependencies: ["OrgEditorAppKit"]),
2527 .testTarget(name: "OrgCoreTests", dependencies: ["OrgCore"]),
2628 .testTarget(name: "OrgDocumentTests", dependencies: ["OrgDocument"]),
2729 .testTarget(name: "OrgIndexTests", dependencies: ["OrgIndex"]),
Sources/OrgstarSpike/main.swift added +89
@@ -0,0 +1,89 @@
1#if os(macOS)
2import AppKit
3import OrgDocument
4import OrgEditorAppKit
5
6/// A throwaway app for trying the editor by hand: `swift run OrgstarSpike path/to/file.org`.
7/// TAB on a heading folds it, Shift-TAB folds everything, ⌘S saves. Edit timings are printed
8/// every 50 edits.
9@MainActor
10final class SpikeDelegate: NSObject, NSApplicationDelegate {
11 let url: URL
12 var editor: OrgEditor?
13 var window: NSWindow?
14 var timings: [Duration] = []
15
16 init(url: URL) {
17 self.url = url
18 }
19
20 func applicationDidFinishLaunching(_ notification: Notification) {
21 let bytes = (try? [UInt8](Data(contentsOf: url))) ?? []
22 let editor = OrgEditor(document: DocumentState(bytes: bytes), frame: NSRect(x: 0, y: 0, width: 900, height: 1000))
23 editor.onEditTiming = { [weak self] in self?.record($0) }
24 let window = NSWindow(
25 contentRect: NSRect(x: 100, y: 100, width: 900, height: 1000),
26 styleMask: [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: false
27 )
28 window.title = url.lastPathComponent
29 window.contentView = editor.makeScrollView()
30 window.makeKeyAndOrderFront(nil)
31 window.makeFirstResponder(editor.textView)
32 self.editor = editor
33 self.window = window
34 buildMenu()
35 NSApp.activate()
36 }
37
38 func buildMenu() {
39 let main = NSMenu()
40 let appItem = NSMenuItem()
41 appItem.submenu = NSMenu()
42 appItem.submenu?.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
43 let fileItem = NSMenuItem()
44 fileItem.submenu = NSMenu(title: "File")
45 fileItem.submenu?.addItem(withTitle: "Save", action: #selector(save), keyEquivalent: "s").target = self
46 let editItem = NSMenuItem()
47 editItem.submenu = NSMenu(title: "Edit")
48 editItem.submenu?.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
49 editItem.submenu?.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z")
50 editItem.submenu?.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
51 editItem.submenu?.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
52 editItem.submenu?.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
53 editItem.submenu?.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
54 [appItem, fileItem, editItem].forEach(main.addItem)
55 NSApp.mainMenu = main
56 }
57
58 @objc func save() {
59 guard let editor else { return }
60 let recovery = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
61 .appendingPathComponent("Orgstar/Recovery")
62 let saver = Saver(fileSystem: CoordinatedFileSystem(), recovery: FileRecoveryStore(directory: recovery))
63 do {
64 print("save:", try editor.save(using: saver, to: url))
65 } catch {
66 print("save failed:", error)
67 }
68 }
69
70 func record(_ duration: Duration) {
71 timings.append(duration)
72 guard timings.count % 50 == 0 else { return }
73 let sorted = timings.sorted()
74 print("edits: \(timings.count) p50 \(sorted[sorted.count / 2]) p95 \(sorted[sorted.count * 95 / 100])")
75 }
76
77 func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
78}
79
80guard CommandLine.arguments.count > 1 else {
81 print("usage: OrgstarSpike path/to/file.org")
82 exit(2)
83}
84let app = NSApplication.shared
85app.setActivationPolicy(.regular)
86let delegate = SpikeDelegate(url: URL(fileURLWithPath: CommandLine.arguments[1]))
87app.delegate = delegate
88app.run()
89#endif