import AppKit import OrgApp import SwiftUI /// Window-level requests the menu bar makes. @MainActor @Observable final class AppCommands { var showingQuickOpen = false var showingFolderPicker = false var focusSearch = false var showingConflict = false } @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { var session: DocumentSession? func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { guard let session else { return .terminateNow } return UnsavedChanges.resolve(session) ? .terminateNow : .terminateCancel } } /// Unsaved edits when leaving a file or quitting. @MainActor enum UnsavedChanges { /// Whether the open file can be left: saved in automatic mode, or as the user chooses in /// explicit mode. A save that fails or conflicts keeps the file open. static func resolve(_ session: DocumentSession) -> Bool { guard session.isDirty else { return true } if session.saveMode == .automatic { return session.save() } let alert = NSAlert() alert.messageText = "Save changes to \(session.path.map { ($0 as NSString).lastPathComponent } ?? "this file")?" alert.informativeText = "Your changes will be lost if you don't save them." alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Don't Save") alert.addButton(withTitle: "Cancel") switch alert.runModal() { case .alertFirstButtonReturn: return session.save() case .alertSecondButtonReturn: return true default: return false } } } @main struct OrgstarApp: App { @NSApplicationDelegateAdaptor private var delegate: AppDelegate @State private var workspace = Result { try WorkspaceModel(directory: WorkspaceModel.defaultDirectory) } @State private var commands = AppCommands() @State private var session = DocumentSession() var body: some Scene { Window("Orgstar", id: "main") { switch workspace { case .success(let model): ContentView(workspace: model, session: session, commands: commands) .onAppear { delegate.session = session } case .failure(let error): ContentUnavailableView("Can't open the index", systemImage: "exclamationmark.triangle", description: Text(error.localizedDescription)) } } .defaultSize(width: 1280, height: 860) .commands { CommandGroup(after: .newItem) { Button("Add Folder…") { commands.showingFolderPicker = true } .keyboardShortcut("o", modifiers: [.command, .shift]) Button("Quick Open…") { commands.showingQuickOpen = true } .keyboardShortcut("p") } CommandGroup(replacing: .saveItem) { Button("Save") { if !session.save(), session.conflict != nil { commands.showingConflict = true } } .keyboardShortcut("s") .disabled(session.path == nil || session.isReadOnly) } CommandGroup(after: .textEditing) { Button("Search Notes") { commands.focusSearch = true } .keyboardShortcut("f", modifiers: [.command, .shift]) } } Settings { SettingsView() } } } struct SettingsView: View { @AppStorage("saveMode") private var saveMode = SaveMode.automatic var body: some View { Form { Picker("Save files:", selection: $saveMode) { Text("Automatically, when typing stops").tag(SaveMode.automatic) Text("Only with File ▸ Save (⌘S)").tag(SaveMode.explicit) } .pickerStyle(.radioGroup) } .padding(20) .frame(width: 420) } }