Add themes selector

This commit is contained in:
2026-03-08 15:03:42 +11:00
parent a6c8218bab
commit 9d05bc586a
9 changed files with 179 additions and 8 deletions

View File

@@ -44,6 +44,7 @@ enum NotchSettings {
// Terminal
static let terminalFontSize = "terminalFontSize"
static let terminalShell = "terminalShell"
static let terminalTheme = "terminalTheme"
// Hotkeys each stores a HotkeyBinding JSON string
static let hotkeyToggle = "hotkey_toggle"
@@ -88,6 +89,7 @@ enum NotchSettings {
static let terminalFontSize: Double = 13
static let terminalShell: String = ""
static let terminalTheme: String = TerminalTheme.terminalApp.rawValue
// Default hotkey bindings as JSON
static let hotkeyToggle: String = HotkeyBinding.cmdReturn.toJSON()
@@ -133,6 +135,7 @@ enum NotchSettings {
Keys.terminalFontSize: Defaults.terminalFontSize,
Keys.terminalShell: Defaults.terminalShell,
Keys.terminalTheme: Defaults.terminalTheme,
Keys.hotkeyToggle: Defaults.hotkeyToggle,
Keys.hotkeyNewTab: Defaults.hotkeyNewTab,

View File

@@ -13,6 +13,8 @@ class TerminalManager: ObservableObject {
@AppStorage(NotchSettings.Keys.terminalFontSize)
private var fontSize: Double = NotchSettings.Defaults.terminalFontSize
@AppStorage(NotchSettings.Keys.terminalTheme)
private var theme: String = NotchSettings.Defaults.terminalTheme
private var cancellables = Set<AnyCancellable>()
@@ -35,7 +37,10 @@ class TerminalManager: ObservableObject {
// MARK: - Tab operations
func newTab() {
let session = TerminalSession(fontSize: CGFloat(fontSize))
let session = TerminalSession(
fontSize: CGFloat(fontSize),
theme: TerminalTheme.resolve(theme)
)
// Forward title changes to trigger view updates in this manager
session.$title
@@ -104,4 +109,10 @@ class TerminalManager: ObservableObject {
tab.updateFontSize(size)
}
}
func updateAllThemes(_ theme: TerminalTheme) {
for tab in tabs {
tab.applyTheme(theme)
}
}
}

View File

@@ -9,25 +9,21 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, Termina
let id = UUID()
let terminalView: TerminalView
private var process: LocalProcess?
private let backgroundColor = NSColor.black
@Published var title: String = "shell"
@Published var isRunning: Bool = true
@Published var currentDirectory: String?
init(fontSize: CGFloat) {
init(fontSize: CGFloat, theme: TerminalTheme) {
terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
super.init()
terminalView.terminalDelegate = self
// Solid black matches every other element in the notch.
// The single `.opacity(notchOpacity)` on ContentView makes
// everything uniformly transparent.
terminalView.nativeBackgroundColor = .black
terminalView.nativeForegroundColor = .init(white: 0.9, alpha: 1.0)
let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
terminalView.font = font
applyTheme(theme)
startShell()
}
@@ -64,6 +60,14 @@ class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, Termina
terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
}
func applyTheme(_ theme: TerminalTheme) {
// Keep the notch visually consistent while swapping the terminal's
// default foreground color and ANSI palette for command output.
terminalView.nativeBackgroundColor = backgroundColor
terminalView.nativeForegroundColor = theme.foregroundColor
terminalView.installColors(theme.ansiColors)
}
func terminate() {
process?.terminate()
process = nil

View File

@@ -0,0 +1,117 @@
import AppKit
import SwiftTerm
enum TerminalTheme: String, CaseIterable, Identifiable {
case terminalApp
case xterm
case solarizedDark
case dracula
case nord
var id: String { rawValue }
var label: String {
switch self {
case .terminalApp: return "Classic"
case .xterm: return "Xterm"
case .solarizedDark:return "Solarized Dark"
case .dracula: return "Dracula"
case .nord: return "Nord"
}
}
var detail: String {
switch self {
case .terminalApp:
return "Matches the app's current terminal palette."
case .xterm:
return "Traditional xterm-style ANSI colors."
case .solarizedDark:
return "Low-contrast dark palette with Solarized accents."
case .dracula:
return "Higher-contrast dark palette with vivid ANSI colors."
case .nord:
return "Cool blue-grey palette with restrained accents."
}
}
var foregroundColor: NSColor {
switch self {
case .terminalApp:
return Self.nsColor(0xE5E5E5)
case .xterm:
return Self.nsColor(0xE5E5E5)
case .solarizedDark:
return Self.nsColor(0x839496)
case .dracula:
return Self.nsColor(0xF8F8F2)
case .nord:
return Self.nsColor(0xD8DEE9)
}
}
var ansiColors: [Color] {
switch self {
case .terminalApp:
return Self.palette([
0x000000, 0xC23621, 0x25BC24, 0xADAD27,
0x492EE1, 0xD338D3, 0x33BBC8, 0xCBCCCD,
0x818383, 0xFC391F, 0x31E722, 0xEAEC23,
0x5833FF, 0xF935F8, 0x14F0F0, 0xE9EBEB
])
case .xterm:
return Self.palette([
0x000000, 0xCD0000, 0x00CD00, 0xCDCD00,
0x0000EE, 0xCD00CD, 0x00CDCD, 0xE5E5E5,
0x7F7F7F, 0xFF0000, 0x00FF00, 0xFFFF00,
0x5C5CFF, 0xFF00FF, 0x00FFFF, 0xFFFFFF
])
case .solarizedDark:
return Self.palette([
0x073642, 0xDC322F, 0x859900, 0xB58900,
0x268BD2, 0xD33682, 0x2AA198, 0xEEE8D5,
0x002B36, 0xCB4B16, 0x586E75, 0x657B83,
0x839496, 0x6C71C4, 0x93A1A1, 0xFDF6E3
])
case .dracula:
return Self.palette([
0x21222C, 0xFF5555, 0x50FA7B, 0xF1FA8C,
0xBD93F9, 0xFF79C6, 0x8BE9FD, 0xF8F8F2,
0x6272A4, 0xFF6E6E, 0x69FF94, 0xFFFFA5,
0xD6ACFF, 0xFF92DF, 0xA4FFFF, 0xFFFFFF
])
case .nord:
return Self.palette([
0x3B4252, 0xBF616A, 0xA3BE8C, 0xEBCB8B,
0x81A1C1, 0xB48EAD, 0x88C0D0, 0xE5E9F0,
0x4C566A, 0xBF616A, 0xA3BE8C, 0xEBCB8B,
0x81A1C1, 0xB48EAD, 0x8FBCBB, 0xECEFF4
])
}
}
static func resolve(_ rawValue: String) -> TerminalTheme {
TerminalTheme(rawValue: rawValue) ?? .terminalApp
}
private static func palette(_ hexValues: [UInt32]) -> [Color] {
hexValues.map(terminalColor)
}
private static func terminalColor(_ hex: UInt32) -> Color {
Color(
red: UInt16(((hex >> 16) & 0xFF) * 257),
green: UInt16(((hex >> 8) & 0xFF) * 257),
blue: UInt16((hex & 0xFF) * 257)
)
}
private static func nsColor(_ hex: UInt32) -> NSColor {
NSColor(
deviceRed: CGFloat((hex >> 16) & 0xFF) / 255.0,
green: CGFloat((hex >> 8) & 0xFF) / 255.0,
blue: CGFloat(hex & 0xFF) / 255.0,
alpha: 1.0
)
}
}