krz/orgstar

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

Sources/OrgstarSpike/main.swift

15f6b0709d88971fb62ed432c3e5b8032643670a
orgstar/Sources/OrgstarSpike/main.swift history · blame · raw

89 lines · 3808 bytes

 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