add Settings window with theme mode and line indicator mode controls

This commit is contained in:
jess 2026-04-05 12:29:04 -07:00
parent 4ae9409eff
commit 0bf2d1b344
5 changed files with 202 additions and 6 deletions

View File

@ -28,8 +28,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.setFrameAutosaveName("SwiftlyMainWindow") window.setFrameAutosaveName("SwiftlyMainWindow")
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
applyThemeAppearance()
setupMenuBar() setupMenuBar()
observeDocumentTitle() observeDocumentTitle()
NotificationCenter.default.addObserver(
self, selector: #selector(settingsDidChange),
name: .settingsChanged, object: nil
)
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
@ -59,6 +65,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let menu = NSMenu() let menu = NSMenu()
menu.addItem(withTitle: "About Swiftly", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") menu.addItem(withTitle: "About Swiftly", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
menu.addItem(.separator()) 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") menu.addItem(withTitle: "Quit Swiftly", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
item.submenu = menu item.submenu = menu
return item return item
@ -163,6 +173,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
@objc private func openSettings() {
SettingsWindowController.show()
}
@objc private func settingsDidChange() {
window.backgroundColor = Theme.current.base
window.contentView?.needsDisplay = true
}
@objc private func toggleSidebar() { @objc private func toggleSidebar() {
NotificationCenter.default.post(name: .toggleSidebar, object: nil) NotificationCenter.default.post(name: .toggleSidebar, object: nil)
} }

View File

@ -3,8 +3,10 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@ObservedObject var state: AppState @ObservedObject var state: AppState
@State private var sidebarVisible: Bool = false @State private var sidebarVisible: Bool = false
@AppStorage("themeMode") private var themeMode: String = "auto"
var body: some View { var body: some View {
let _ = themeMode
HSplitView { HSplitView {
if sidebarVisible { if sidebarVisible {
SidebarView(state: state) SidebarView(state: state)

View File

@ -818,6 +818,7 @@ struct EditorTextView: NSViewRepresentable {
private var isUpdatingTables = false private var isUpdatingTables = false
private var embeddedTableViews: [MarkdownTableView] = [] private var embeddedTableViews: [MarkdownTableView] = []
private var focusObserver: NSObjectProtocol? private var focusObserver: NSObjectProtocol?
private var settingsObserver: NSObjectProtocol?
init(_ parent: EditorTextView) { init(_ parent: EditorTextView) {
self.parent = parent self.parent = parent
@ -829,12 +830,32 @@ struct EditorTextView: NSViewRepresentable {
tv.window?.makeFirstResponder(tv) tv.window?.makeFirstResponder(tv)
tv.setSelectedRange(NSRange(location: 0, length: 0)) tv.setSelectedRange(NSRange(location: 0, length: 0))
} }
settingsObserver = NotificationCenter.default.addObserver(
forName: .settingsChanged, object: nil, queue: .main
) { [weak self] _ in
guard let tv = self?.textView, let ts = tv.textStorage else { return }
let palette = Theme.current
tv.backgroundColor = palette.base
tv.insertionPointColor = palette.text
tv.selectedTextAttributes = [.backgroundColor: palette.surface1]
tv.typingAttributes = [
.font: Theme.editorFont,
.foregroundColor: palette.text
]
ts.beginEditing()
applySyntaxHighlighting(to: ts)
ts.endEditing()
tv.needsDisplay = true
}
} }
deinit { deinit {
if let obs = focusObserver { if let obs = focusObserver {
NotificationCenter.default.removeObserver(obs) NotificationCenter.default.removeObserver(obs)
} }
if let obs = settingsObserver {
NotificationCenter.default.removeObserver(obs)
}
} }
func textDidChange(_ notification: Notification) { func textDidChange(_ notification: Notification) {
@ -1982,6 +2003,8 @@ class LineNumberTextView: NSTextView {
let text = string as NSString let text = string as NSString
guard text.length > 0 else { return } guard text.length > 0 else { return }
let lineMode = UserDefaults.standard.string(forKey: "lineIndicatorMode") ?? "on"
var containerVisible = visibleRect var containerVisible = visibleRect
containerVisible.origin.x -= origin.x containerVisible.origin.x -= origin.x
containerVisible.origin.y -= origin.y containerVisible.origin.y -= origin.y
@ -1995,10 +2018,24 @@ class LineNumberTextView: NSTextView {
idx += 1 idx += 1
} }
var cursorLine = 1
if lineMode == "vim" {
let cursorPos = selectedRange().location
var ci = 0
while ci < min(cursorPos, text.length) {
if text.character(at: ci) == 0x0A { cursorLine += 1 }
ci += 1
}
}
let lineAttrs: [NSAttributedString.Key: Any] = [ let lineAttrs: [NSAttributedString.Key: Any] = [
.font: Theme.gutterFont, .font: Theme.gutterFont,
.foregroundColor: palette.overlay0 .foregroundColor: palette.overlay0
] ]
let currentLineAttrs: [NSAttributedString.Key: Any] = [
.font: Theme.gutterFont,
.foregroundColor: palette.text
]
let resultAttrs: [NSAttributedString.Key: Any] = [ let resultAttrs: [NSAttributedString.Key: Any] = [
.font: Theme.gutterFont, .font: Theme.gutterFont,
.foregroundColor: palette.teal .foregroundColor: palette.teal
@ -2011,9 +2048,25 @@ class LineNumberTextView: NSTextView {
let lineRect = lm.boundingRect(forGlyphRange: lineGlyphRange, in: tc) let lineRect = lm.boundingRect(forGlyphRange: lineGlyphRange, in: tc)
let y = lineRect.origin.y + origin.y let y = lineRect.origin.y + origin.y
let numStr = NSAttributedString(string: "\(lineNumber)", attributes: lineAttrs) if lineMode != "off" {
let displayNum: Int
let attrs: [NSAttributedString.Key: Any]
if lineMode == "vim" {
if lineNumber == cursorLine {
displayNum = lineNumber
attrs = currentLineAttrs
} else {
displayNum = abs(lineNumber - cursorLine)
attrs = lineAttrs
}
} else {
displayNum = lineNumber
attrs = lineAttrs
}
let numStr = NSAttributedString(string: "\(displayNum)", attributes: attrs)
let numSize = numStr.size() let numSize = numStr.size()
numStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - numSize.width - 8, y: y)) numStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - numSize.width - 8, y: y))
}
if let result = evalResults[lineNumber - 1] { if let result = evalResults[lineNumber - 1] {
let resultStr = NSAttributedString(string: "\u{2192} \(result)", attributes: resultAttrs) let resultStr = NSAttributedString(string: "\u{2192} \(result)", attributes: resultAttrs)

116
src/SettingsView.swift Normal file
View File

@ -0,0 +1,116 @@
import SwiftUI
import Cocoa
enum ThemeMode: String, CaseIterable {
case auto = "auto"
case dark = "dark"
case light = "light"
var label: String {
switch self {
case .auto: return "Auto"
case .dark: return "Dark"
case .light: return "Light"
}
}
}
enum LineIndicatorMode: String, CaseIterable {
case on = "on"
case off = "off"
case vim = "vim"
var label: String {
switch self {
case .on: return "On"
case .off: return "Off"
case .vim: return "Vim"
}
}
}
struct SettingsView: View {
@AppStorage("themeMode") private var themeMode: String = "auto"
@AppStorage("lineIndicatorMode") private var lineIndicatorMode: String = "on"
var body: some View {
let palette = Theme.current
Form {
Section("Theme") {
Picker("Mode", selection: $themeMode) {
ForEach(ThemeMode.allCases, id: \.rawValue) { mode in
Text(mode.label).tag(mode.rawValue)
}
}
.pickerStyle(.segmented)
}
Section("Line Numbers") {
Picker("Mode", selection: $lineIndicatorMode) {
ForEach(LineIndicatorMode.allCases, id: \.rawValue) { mode in
Text(mode.label).tag(mode.rawValue)
}
}
.pickerStyle(.segmented)
}
}
.formStyle(.grouped)
.frame(width: 320, height: 180)
.background(Color(ns: palette.base))
.onChange(of: themeMode) {
DispatchQueue.main.async {
applyThemeAppearance()
NotificationCenter.default.post(name: .settingsChanged, object: nil)
}
}
.onChange(of: lineIndicatorMode) {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .settingsChanged, object: nil)
}
}
}
}
func applyThemeAppearance() {
let mode = UserDefaults.standard.string(forKey: "themeMode") ?? "auto"
switch mode {
case "dark":
NSApp.appearance = NSAppearance(named: .darkAqua)
case "light":
NSApp.appearance = NSAppearance(named: .aqua)
default:
NSApp.appearance = nil
}
}
extension Notification.Name {
static let settingsChanged = Notification.Name("settingsChanged")
}
class SettingsWindowController {
private static var window: NSWindow?
static func show() {
if let existing = window, existing.isVisible {
existing.makeKeyAndOrderFront(nil)
return
}
let settingsView = SettingsView()
let hostingView = NSHostingView(rootView: settingsView)
hostingView.frame = NSRect(x: 0, y: 0, width: 320, height: 200)
let w = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 320, height: 200),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
w.title = "Settings"
w.contentView = hostingView
w.center()
w.isReleasedWhenClosed = false
w.makeKeyAndOrderFront(nil)
window = w
}
}

View File

@ -90,10 +90,16 @@ struct Theme {
) )
static var current: CatppuccinPalette { static var current: CatppuccinPalette {
let mode = UserDefaults.standard.string(forKey: "themeMode") ?? "auto"
switch mode {
case "dark": return mocha
case "light": return latte
default:
let appearance = NSApp.effectiveAppearance let appearance = NSApp.effectiveAppearance
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
return isDark ? mocha : latte return isDark ? mocha : latte
} }
}
static let editorFont = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular) static let editorFont = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
static let gutterFont = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular) static let gutterFont = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)