krz/orgstar

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

Sources/Orgstar/OrgstarApp.swift

056befa97fffa110959cb6010c1986461c09dc0b
orgstar/Sources/Orgstar/OrgstarApp.swift history · blame · raw

106 lines · 3998 bytes

  1import AppKit
  2import OrgApp
  3import SwiftUI
  4
  5/// Window-level requests the menu bar makes.
  6@MainActor
  7@Observable
  8final class AppCommands {
  9    var showingQuickOpen = false
 10    var showingFolderPicker = false
 11    var focusSearch = false
 12    var showingConflict = false
 13}
 14
 15@MainActor
 16final class AppDelegate: NSObject, NSApplicationDelegate {
 17    var session: DocumentSession?
 18
 19    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
 20
 21    func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
 22        guard let session else { return .terminateNow }
 23        return UnsavedChanges.resolve(session) ? .terminateNow : .terminateCancel
 24    }
 25}
 26
 27/// Unsaved edits when leaving a file or quitting.
 28@MainActor
 29enum UnsavedChanges {
 30    /// Whether the open file can be left: saved in automatic mode, or as the user chooses in
 31    /// explicit mode. A save that fails or conflicts keeps the file open.
 32    static func resolve(_ session: DocumentSession) -> Bool {
 33        guard session.isDirty else { return true }
 34        if session.saveMode == .automatic { return session.save() }
 35        let alert = NSAlert()
 36        alert.messageText = "Save changes to \(session.path.map { ($0 as NSString).lastPathComponent } ?? "this file")?"
 37        alert.informativeText = "Your changes will be lost if you don't save them."
 38        alert.addButton(withTitle: "Save")
 39        alert.addButton(withTitle: "Don't Save")
 40        alert.addButton(withTitle: "Cancel")
 41        switch alert.runModal() {
 42        case .alertFirstButtonReturn: return session.save()
 43        case .alertSecondButtonReturn: return true
 44        default: return false
 45        }
 46    }
 47}
 48
 49@main
 50struct OrgstarApp: App {
 51    @NSApplicationDelegateAdaptor private var delegate: AppDelegate
 52    @State private var workspace = Result { try WorkspaceModel(directory: WorkspaceModel.defaultDirectory) }
 53    @State private var commands = AppCommands()
 54    @State private var session = DocumentSession()
 55
 56    var body: some Scene {
 57        Window("Orgstar", id: "main") {
 58            switch workspace {
 59            case .success(let model):
 60                ContentView(workspace: model, session: session, commands: commands)
 61                    .onAppear { delegate.session = session }
 62            case .failure(let error):
 63                ContentUnavailableView("Can't open the index", systemImage: "exclamationmark.triangle", description: Text(error.localizedDescription))
 64            }
 65        }
 66        .defaultSize(width: 1280, height: 860)
 67        .commands {
 68            CommandGroup(after: .newItem) {
 69                Button("Add Folder…") { commands.showingFolderPicker = true }
 70                    .keyboardShortcut("o", modifiers: [.command, .shift])
 71                Button("Quick Open…") { commands.showingQuickOpen = true }
 72                    .keyboardShortcut("p")
 73            }
 74            CommandGroup(replacing: .saveItem) {
 75                Button("Save") {
 76                    if !session.save(), session.conflict != nil { commands.showingConflict = true }
 77                }
 78                .keyboardShortcut("s")
 79                .disabled(session.path == nil || session.isReadOnly)
 80            }
 81            CommandGroup(after: .textEditing) {
 82                Button("Search Notes") { commands.focusSearch = true }
 83                    .keyboardShortcut("f", modifiers: [.command, .shift])
 84            }
 85        }
 86        Settings {
 87            SettingsView()
 88        }
 89    }
 90}
 91
 92struct SettingsView: View {
 93    @AppStorage("saveMode") private var saveMode = SaveMode.automatic
 94
 95    var body: some View {
 96        Form {
 97            Picker("Save files:", selection: $saveMode) {
 98                Text("Automatically, when typing stops").tag(SaveMode.automatic)
 99                Text("Only with File ▸ Save (⌘S)").tag(SaveMode.explicit)
100            }
101            .pickerStyle(.radioGroup)
102        }
103        .padding(20)
104        .frame(width: 420)
105    }
106}