import Cocoa import Combine import SwiftUI import UniformTypeIdentifiers class WindowController { let window: NSWindow let appState: AppState init(window: NSWindow, appState: AppState) { self.window = window self.appState = appState } } class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! var appState: AppState! private var titleCancellable: AnyCancellable? private var titleBarView: TitleBarView? private var focusTitleObserver: NSObjectProtocol? private var windowControllers: [WindowController] = [] func applicationDidFinishLaunching(_ notification: Notification) { _ = ConfigManager.shared appState = AppState() let contentView = ContentView(state: appState) let hostingView = NSHostingView(rootView: contentView) window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false ) window.titlebarAppearsTransparent = true window.titleVisibility = .hidden window.backgroundColor = Theme.current.base window.title = "Swiftly" window.contentView = hostingView window.center() window.setFrameAutosaveName("SwiftlyMainWindow") window.makeKeyAndOrderFront(nil) applyThemeAppearance() setupTitleBar() setupMenuBar() observeDocumentTitle() DocumentBrowserController.shared = DocumentBrowserController(appState: appState) NotificationCenter.default.addObserver( self, selector: #selector(settingsDidChange), name: .settingsChanged, object: nil ) if let url = pendingOpenURLs.first { pendingOpenURLs = [] appState.loadNoteFromFile(url) } } private var pendingOpenURLs: [URL] = [] func application(_ application: NSApplication, open urls: [URL]) { guard let url = urls.first else { return } if appState != nil { appState.loadNoteFromFile(url) } else { pendingOpenURLs = [url] } } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } func applicationWillTerminate(_ notification: Notification) { appState.saveNote() } // MARK: - Menu bar private func setupMenuBar() { let mainMenu = NSMenu() mainMenu.addItem(buildAppMenu()) mainMenu.addItem(buildFileMenu()) mainMenu.addItem(buildEditMenu()) mainMenu.addItem(buildViewMenu()) mainMenu.addItem(buildWindowMenu()) NSApp.mainMenu = mainMenu } private func buildAppMenu() -> NSMenuItem { let item = NSMenuItem() let menu = NSMenu() menu.addItem(withTitle: "About Swiftly", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") menu.addItem(.separator()) let settingsItem = NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ",") settingsItem.target = self menu.addItem(settingsItem) menu.addItem(.separator()) menu.addItem(withTitle: "Quit Swiftly", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") item.submenu = menu return item } private func buildFileMenu() -> NSMenuItem { let item = NSMenuItem() let menu = NSMenu(title: "File") let newWindowItem = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "n") newWindowItem.target = self menu.addItem(newWindowItem) let newNoteItem = NSMenuItem(title: "New Note", action: #selector(newNote), keyEquivalent: "N") newNoteItem.keyEquivalentModifierMask = [.command, .shift] newNoteItem.target = self menu.addItem(newNoteItem) let openItem = NSMenuItem(title: "Open...", action: #selector(openNote), keyEquivalent: "o") openItem.target = self menu.addItem(openItem) menu.addItem(.separator()) let saveItem = NSMenuItem(title: "Save", action: #selector(saveNote), keyEquivalent: "s") saveItem.target = self menu.addItem(saveItem) let saveAsItem = NSMenuItem(title: "Save As...", action: #selector(saveNoteAs), keyEquivalent: "S") saveAsItem.target = self menu.addItem(saveAsItem) menu.addItem(.separator()) let openStorageItem = NSMenuItem(title: "Open Storage Directory", action: #selector(openStorageDirectory), keyEquivalent: "") openStorageItem.target = self menu.addItem(openStorageItem) item.submenu = menu return item } private func buildEditMenu() -> NSMenuItem { let item = NSMenuItem() let menu = NSMenu(title: "Edit") menu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z") menu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z") menu.addItem(.separator()) menu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x") menu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c") menu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v") menu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a") menu.addItem(.separator()) let boldItem = NSMenuItem(title: "Bold", action: #selector(boldSelection), keyEquivalent: "b") boldItem.target = self menu.addItem(boldItem) let italicItem = NSMenuItem(title: "Italic", action: #selector(italicizeSelection), keyEquivalent: "i") italicItem.target = self menu.addItem(italicItem) menu.addItem(.separator()) let tableItem = NSMenuItem(title: "Insert Table", action: #selector(insertTable), keyEquivalent: "t") tableItem.target = self menu.addItem(tableItem) let evalItem = NSMenuItem(title: "Smart Eval", action: #selector(smartEval), keyEquivalent: "e") evalItem.target = self menu.addItem(evalItem) menu.addItem(.separator()) let findItem = NSMenuItem(title: "Find...", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "f") findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue) menu.addItem(findItem) menu.addItem(.separator()) let formatItem = NSMenuItem(title: "Format Document", action: #selector(formatDocument), keyEquivalent: "F") formatItem.keyEquivalentModifierMask = [.command, .shift] formatItem.target = self menu.addItem(formatItem) item.submenu = menu return item } private func buildViewMenu() -> NSMenuItem { let item = NSMenuItem() let menu = NSMenu(title: "View") let toggleItem = NSMenuItem(title: "Document Browser", action: #selector(toggleBrowser), keyEquivalent: "b") toggleItem.keyEquivalentModifierMask = .control toggleItem.target = self menu.addItem(toggleItem) menu.addItem(.separator()) let zoomInItem = NSMenuItem(title: "Zoom In", action: #selector(zoomIn), keyEquivalent: "=") zoomInItem.target = self menu.addItem(zoomInItem) let zoomOutItem = NSMenuItem(title: "Zoom Out", action: #selector(zoomOut), keyEquivalent: "-") zoomOutItem.target = self menu.addItem(zoomOutItem) let actualSizeItem = NSMenuItem(title: "Actual Size", action: #selector(zoomReset), keyEquivalent: "0") actualSizeItem.target = self menu.addItem(actualSizeItem) item.submenu = menu return item } private func buildWindowMenu() -> NSMenuItem { let item = NSMenuItem() let menu = NSMenu(title: "Window") menu.addItem(withTitle: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m") menu.addItem(withTitle: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: "") item.submenu = menu NSApp.windowsMenu = menu return item } // MARK: - Actions @objc private func newNote() { appState.newNote() } @objc private func newWindow() { let state = AppState() let contentView = ContentView(state: state) let hostingView = NSHostingView(rootView: contentView) let win = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false ) win.titlebarAppearsTransparent = true win.titleVisibility = .hidden win.backgroundColor = Theme.current.base win.title = "Swiftly" win.contentView = hostingView win.center() win.makeKeyAndOrderFront(nil) let controller = WindowController(window: win, appState: state) windowControllers.append(controller) } @objc private func openStorageDirectory() { let dir = ConfigManager.shared.autoSaveDirectory let url = URL(fileURLWithPath: dir, isDirectory: true) NSWorkspace.shared.open(url) } @objc private func boldSelection() { NotificationCenter.default.post(name: .boldSelection, object: nil) } @objc private func italicizeSelection() { NotificationCenter.default.post(name: .italicizeSelection, object: nil) } @objc private func insertTable() { NotificationCenter.default.post(name: .insertTable, object: nil) } @objc private func smartEval() { NotificationCenter.default.post(name: .smartEval, object: nil) } @objc private func openNote() { let panel = NSOpenPanel() panel.allowedContentTypes = Self.supportedContentTypes panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = false panel.beginSheetModal(for: window) { [weak self] response in guard response == .OK, let url = panel.url else { return } self?.appState.loadNoteFromFile(url) } } @objc private func saveNote() { if appState.currentFileURL != nil { appState.saveNote() } else { saveNoteAs() } } @objc private func saveNoteAs() { let panel = NSSavePanel() panel.allowedContentTypes = Self.supportedContentTypes panel.nameFieldStringValue = defaultFilename() if let url = appState.currentFileURL { panel.directoryURL = url.deletingLastPathComponent() panel.nameFieldStringValue = url.lastPathComponent } panel.beginSheetModal(for: window) { [weak self] response in guard response == .OK, let url = panel.url else { return } self?.appState.saveNoteToFile(url) } } private func defaultFilename() -> String { if let url = appState.currentFileURL { return url.lastPathComponent } let firstLine = appState.documentText .components(separatedBy: "\n").first? .trimmingCharacters(in: .whitespaces) ?? "" let stripped = firstLine.replacingOccurrences( of: "^#+\\s*", with: "", options: .regularExpression ) let trimmed = stripped.trimmingCharacters(in: .whitespaces) let ext = extensionForFormat(appState.currentFileFormat) guard !trimmed.isEmpty, trimmed != "Untitled" else { return "note.\(ext)" } let sanitized = trimmed.map { "/:\\\\".contains($0) ? "-" : String($0) }.joined() return sanitized.prefix(80) + ".\(ext)" } private func extensionForFormat(_ format: FileFormat) -> String { switch format { case .markdown: return "md" case .csv: return "csv" case .json: return "json" case .toml: return "toml" case .yaml: return "yaml" case .xml: return "xml" case .svg: return "svg" case .rust: return "rs" case .c: return "c" case .cpp: return "cpp" case .objc: return "m" case .javascript: return "js" case .typescript: return "ts" case .jsx: return "jsx" case .tsx: return "tsx" case .html: return "html" case .css: return "css" case .scss: return "scss" case .less: return "less" case .python: return "py" case .go: return "go" case .ruby: return "rb" case .php: return "php" case .lua: return "lua" case .shell: return "sh" case .java: return "java" case .kotlin: return "kt" case .swift: return "swift" case .zig: return "zig" case .sql: return "sql" case .makefile: return "mk" case .dockerfile: return "Dockerfile" case .config: return "conf" case .lock: return "lock" case .plainText, .unknown: return "txt" } } private static let supportedContentTypes: [UTType] = { let extensions = [ "md", "markdown", "mdown", "csv", "json", "toml", "yaml", "yml", "xml", "svg", "rs", "c", "cpp", "cc", "cxx", "h", "hpp", "hxx", "js", "jsx", "ts", "tsx", "html", "htm", "css", "scss", "less", "py", "go", "rb", "php", "lua", "sh", "bash", "zsh", "fish", "java", "kt", "kts", "swift", "zig", "sql", "mk", "ini", "cfg", "conf", "env", "lock", "txt", "text", "log" ] var types: [UTType] = [.plainText] for ext in extensions { if let t = UTType(filenameExtension: ext) { types.append(t) } } return Array(Set(types)) }() @objc private func formatDocument() { NotificationCenter.default.post(name: .formatDocument, object: nil) } @objc private func openSettings() { SettingsWindowController.show() } @objc private func settingsDidChange() { window.backgroundColor = Theme.current.base window.contentView?.needsDisplay = true } @objc private func toggleBrowser() { DocumentBrowserController.shared?.toggle() } @objc private func zoomIn() { if let browser = DocumentBrowserController.shared, browser.window.isKeyWindow { browser.browserState.scaleUp() return } ConfigManager.shared.zoomLevel += 1 NotificationCenter.default.post(name: .settingsChanged, object: nil) } @objc private func zoomOut() { if let browser = DocumentBrowserController.shared, browser.window.isKeyWindow { browser.browserState.scaleDown() return } let current = ConfigManager.shared.zoomLevel if 11 + current > 8 { ConfigManager.shared.zoomLevel -= 1 NotificationCenter.default.post(name: .settingsChanged, object: nil) } } @objc private func zoomReset() { ConfigManager.shared.zoomLevel = 0 NotificationCenter.default.post(name: .settingsChanged, object: nil) } private func setupTitleBar() { let accessory = TitleBarAccessoryController() window.addTitlebarAccessoryViewController(accessory) let tbv = accessory.titleView tbv.onCommit = { [weak self] rawTitle in guard let self = self else { return } let lines = self.appState.documentText.components(separatedBy: "\n") var rest = Array(lines.dropFirst()) if rest.isEmpty && self.appState.documentText.isEmpty { rest = [] } self.appState.documentText = ([rawTitle] + rest).joined(separator: "\n") } titleBarView = tbv focusTitleObserver = NotificationCenter.default.addObserver( forName: .focusTitle, object: nil, queue: .main ) { [weak self] _ in self?.titleBarView?.beginEditing() } } private func observeDocumentTitle() { titleCancellable = appState.$documentText .receive(on: RunLoop.main) .sink { [weak self] text in guard let self = self else { return } let firstLine = text.components(separatedBy: "\n").first? .trimmingCharacters(in: .whitespaces) ?? "" let clean = firstLine.replacingOccurrences( of: "^#+\\s*", with: "", options: .regularExpression ) let displayTitle = clean.isEmpty ? "Swiftly" : String(clean.prefix(60)) self.window.title = displayTitle self.titleBarView?.title = firstLine } } }