import AppKit /// App-wide editor appearance: theme, font family, font size, soft-wrap and /// line-number toggles. Persisted in UserDefaults and broadcast so every open /// document window updates together. enum AppSettings { static let themeKey = "editorThemeID" static let fontNameKey = "editorFontName" static let fontSizeKey = "editorFontSize" static let softWrapKey = "editorSoftWrap" static let lineNumbersKey = "editorLineNumbers" static let boardPageSizeKey = "boardPageSize" static let boardOrientationKey = "boardOrientation" static let boardSnapKey = "boardSnap" static let didChange = Notification.Name("TextPlusAppearanceDidChange") static var theme: EditorTheme { EditorTheme.byID(UserDefaults.standard.string(forKey: themeKey)) } static var fontSize: CGFloat { let s = UserDefaults.standard.double(forKey: fontSizeKey) return s >= 6 ? CGFloat(s) : 13 } static var fontName: String { UserDefaults.standard.string(forKey: fontNameKey) ?? "SF Mono" } static var softWrap: Bool { UserDefaults.standard.object(forKey: softWrapKey) as? Bool ?? true } static var lineNumbers: Bool { UserDefaults.standard.object(forKey: lineNumbersKey) as? Bool ?? true } static var boardPageSize: String { UserDefaults.standard.string(forKey: boardPageSizeKey) ?? "letter" } static var boardOrientation: PageOrientation { PageOrientation(rawValue: UserDefaults.standard.string(forKey: boardOrientationKey) ?? "portrait") ?? .portrait } static var boardSnap: Bool { UserDefaults.standard.object(forKey: boardSnapKey) as? Bool ?? true } static var editorFont: NSFont { font(named: fontName, size: fontSize) } static func font(named name: String, size: CGFloat) -> NSFont { if name == "SF Mono" { return NSFont.monospacedSystemFont(ofSize: size, weight: .regular) } if name == "System" { return NSFont.systemFont(ofSize: size) } return NSFont(name: name, size: size) ?? NSFont.monospacedSystemFont(ofSize: size, weight: .regular) } /// Curated font menu — monospaced first (best for code/scraping), then a /// couple of proportional faces for prose. static let fontChoices: [String] = [ "SF Mono", "Menlo", "Monaco", "Courier New", "System", "Helvetica Neue", "Georgia", ] static func set(_ key: String, _ value: Any) { UserDefaults.standard.set(value, forKey: key) NotificationCenter.default.post(name: didChange, object: nil) } } /// A complete editor color scheme. Markdown preview and the line-number gutter /// derive their colors from the same palette so everything stays consistent. struct EditorTheme: Equatable { let id: String let name: String let isDark: Bool let background: NSColor let foreground: NSColor let caret: NSColor let selection: NSColor let gutterBackground: NSColor let gutterText: NSColor let currentLine: NSColor let accent: NSColor let secondaryText: NSColor static func rgb(_ r: Int, _ g: Int, _ b: Int, _ a: CGFloat = 1) -> NSColor { NSColor(srgbRed: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: a) } static let system = EditorTheme( id: "system", name: "System", isDark: false, background: .textBackgroundColor, foreground: .textColor, caret: .textColor, selection: .selectedTextBackgroundColor, gutterBackground: rgb(245, 245, 247), gutterText: rgb(160, 160, 168), currentLine: rgb(0, 0, 0, 0.04), accent: .controlAccentColor, secondaryText: .secondaryLabelColor) static let dark = EditorTheme( id: "dark", name: "Midnight", isDark: true, background: rgb(24, 26, 31), foreground: rgb(222, 226, 233), caret: rgb(120, 200, 255), selection: rgb(56, 84, 130), gutterBackground: rgb(19, 21, 25), gutterText: rgb(95, 102, 115), currentLine: rgb(255, 255, 255, 0.05), accent: rgb(120, 200, 255), secondaryText: rgb(150, 158, 172)) static let sepia = EditorTheme( id: "sepia", name: "Sepia", isDark: false, background: rgb(247, 240, 224), foreground: rgb(60, 50, 38), caret: rgb(150, 90, 40), selection: rgb(220, 200, 160), gutterBackground: rgb(240, 231, 211), gutterText: rgb(170, 150, 120), currentLine: rgb(120, 90, 40, 0.07), accent: rgb(165, 100, 40), secondaryText: rgb(130, 112, 86)) static let solarized = EditorTheme( id: "solarized", name: "Solarized", isDark: true, background: rgb(0, 43, 54), foreground: rgb(147, 161, 161), caret: rgb(38, 139, 210), selection: rgb(7, 54, 66), gutterBackground: rgb(0, 37, 46), gutterText: rgb(88, 110, 117), currentLine: rgb(255, 255, 255, 0.04), accent: rgb(42, 161, 152), secondaryText: rgb(101, 123, 131)) static let nord = EditorTheme( id: "nord", name: "Nord", isDark: true, background: rgb(46, 52, 64), foreground: rgb(216, 222, 233), caret: rgb(136, 192, 208), selection: rgb(67, 76, 94), gutterBackground: rgb(40, 45, 56), gutterText: rgb(110, 120, 138), currentLine: rgb(255, 255, 255, 0.05), accent: rgb(143, 188, 187), secondaryText: rgb(150, 160, 178)) static let all: [EditorTheme] = [system, dark, sepia, solarized, nord] static func byID(_ id: String?) -> EditorTheme { all.first { $0.id == id } ?? system } var appearance: NSAppearance? { NSAppearance(named: isDark ? .darkAqua : .aqua) } }