A no-Xcode AppKit app (swiftc + build.sh): - Plaintext editor: 5 themes, line numbers, soft wrap, font controls, find, live Markdown preview (split mode), selection-aware Markdown toolbar. - Freeform Board mode: zoomable/pannable canvas of page cards (portrait/landscape, US Letter/A4/Legal/Square/Wide/Note), clean boxes, images (paste/drop/panel), freehand pen/shapes, screen-grab eyedropper, nested right-click menu, rich status bar. The Editor follows the selected page; layout persists to a foo.txt.board sidecar; the .txt stays pure plaintext. - scp-on-save to a Tailscale Mac (key auth, in-flight/quit drain). - HTML scraping pipeline: custom selector engine + Discogs profile, ingests to Postgres (psql) into a marketplace_snapshots schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
5.6 KiB
Swift
148 lines
5.6 KiB
Swift
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)
|
|
}
|
|
}
|