textplus/Sources/Appearance.swift
monster 09972ccde1 TextPlus: native macOS plaintext editor with sync, scraping, and a freeform board
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>
2026-06-14 16:08:59 +10:00

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)
}
}