diff --git a/.gitignore b/.gitignore index 4007749..9ffbc2f 100644 --- a/.gitignore +++ b/.gitignore @@ -80,5 +80,6 @@ dist/ build/ **/Release* +CommandNotch 20* **/.DS_Store \ No newline at end of file diff --git a/Downterm/CommandNotch.xcodeproj/project.pbxproj b/Downterm/CommandNotch.xcodeproj/project.pbxproj index c6cdacb..5e5493a 100644 --- a/Downterm/CommandNotch.xcodeproj/project.pbxproj +++ b/Downterm/CommandNotch.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 26A767A10DDA77A690CC3C37 /* NotchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589421631401C819FE1A7BA9 /* NotchViewModel.swift */; }; 295653929D5B9C0E6C90D6D7 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 032AECA58EA4C274BE9F3320 /* SwiftTerm */; }; 37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B598809B19C892470DE7268 /* TerminalSession.swift */; }; + 3A1F0C4BE9D84A5C8E2B7101 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */; }; 4566E6B87CB62AF5C8D4B9D8 /* NotchShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC09C538CBE7C2D072008B2 /* NotchShape.swift */; }; 4D5125E11B4DDBDB3DFACBAF /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B72743F178231E0B06DD3DE /* HotkeyManager.swift */; }; 5B14FC23928E817FEB8D2A74 /* NotchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FEFF9074A85F02C43D9408 /* NotchWindow.swift */; }; @@ -50,6 +51,7 @@ 5C0779490DE9020FBBC464BE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 665CFC051CF185B71199608D /* CommandNotch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CommandNotch.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7B598809B19C892470DE7268 /* TerminalSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSession.swift; sourceTree = ""; }; + 3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = ""; }; 9547A79F60E46F4521A70674 /* CommandNotch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CommandNotch.entitlements; sourceTree = ""; }; AA6359CF9DDF89413440300D /* NotchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchSettings.swift; sourceTree = ""; }; BA6843B571B41986DE386F5F /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = ""; }; @@ -109,6 +111,7 @@ 589421631401C819FE1A7BA9 /* NotchViewModel.swift */, BA6843B571B41986DE386F5F /* TerminalManager.swift */, 7B598809B19C892470DE7268 /* TerminalSession.swift */, + 3A1F0C4AE9D84A5C8E2B7101 /* TerminalTheme.swift */, ); path = Models; sourceTree = ""; @@ -255,6 +258,7 @@ 7EA51C3720BED7E6189E057D /* TabBar.swift in Sources */, E9A064422790735E033E534F /* TerminalManager.swift in Sources */, 37FC0A7CEEA37C9DCC6A8351 /* TerminalSession.swift in Sources */, + 3A1F0C4BE9D84A5C8E2B7101 /* TerminalTheme.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Downterm/CommandNotch.xcodeproj/project.xcworkspace/xcuserdata/harvmaster.xcuserdatad/UserInterfaceState.xcuserstate b/Downterm/CommandNotch.xcodeproj/project.xcworkspace/xcuserdata/harvmaster.xcuserdatad/UserInterfaceState.xcuserstate index ae272fa..17f51ba 100644 Binary files a/Downterm/CommandNotch.xcodeproj/project.xcworkspace/xcuserdata/harvmaster.xcuserdatad/UserInterfaceState.xcuserstate and b/Downterm/CommandNotch.xcodeproj/project.xcworkspace/xcuserdata/harvmaster.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Downterm/CommandNotch/AppDelegate.swift b/Downterm/CommandNotch/AppDelegate.swift index 1b8a522..fd6f4e6 100644 --- a/Downterm/CommandNotch/AppDelegate.swift +++ b/Downterm/CommandNotch/AppDelegate.swift @@ -19,6 +19,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { observeDisplayPreference() observeSizePreferences() observeFontSizeChanges() + observeTerminalThemeChanges() } func applicationWillTerminate(_ notification: Notification) { @@ -58,6 +59,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { } .store(in: &cancellables) } + + /// Live-update terminal colors across all sessions. + private func observeTerminalThemeChanges() { + UserDefaults.standard.publisher(for: \.terminalTheme) + .removeDuplicates() + .sink { newTheme in + TerminalManager.shared.updateAllThemes(TerminalTheme.resolve(newTheme)) + } + .store(in: &cancellables) + } } // MARK: - KVO key paths @@ -67,6 +78,10 @@ private extension UserDefaults { double(forKey: NotchSettings.Keys.terminalFontSize) } + @objc var terminalTheme: String { + string(forKey: NotchSettings.Keys.terminalTheme) ?? NotchSettings.Defaults.terminalTheme + } + @objc var showOnAllDisplays: Bool { bool(forKey: NotchSettings.Keys.showOnAllDisplays) } diff --git a/Downterm/CommandNotch/Models/NotchSettings.swift b/Downterm/CommandNotch/Models/NotchSettings.swift index e8c7d0b..892c258 100644 --- a/Downterm/CommandNotch/Models/NotchSettings.swift +++ b/Downterm/CommandNotch/Models/NotchSettings.swift @@ -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, diff --git a/Downterm/CommandNotch/Models/TerminalManager.swift b/Downterm/CommandNotch/Models/TerminalManager.swift index 1ac4a68..6d32dbe 100644 --- a/Downterm/CommandNotch/Models/TerminalManager.swift +++ b/Downterm/CommandNotch/Models/TerminalManager.swift @@ -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() @@ -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) + } + } } diff --git a/Downterm/CommandNotch/Models/TerminalSession.swift b/Downterm/CommandNotch/Models/TerminalSession.swift index 5bdbfac..edbf98f 100644 --- a/Downterm/CommandNotch/Models/TerminalSession.swift +++ b/Downterm/CommandNotch/Models/TerminalSession.swift @@ -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 diff --git a/Downterm/CommandNotch/Models/TerminalTheme.swift b/Downterm/CommandNotch/Models/TerminalTheme.swift new file mode 100644 index 0000000..5749411 --- /dev/null +++ b/Downterm/CommandNotch/Models/TerminalTheme.swift @@ -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 + ) + } +} diff --git a/Downterm/CommandNotch/Views/SettingsView.swift b/Downterm/CommandNotch/Views/SettingsView.swift index 20514a6..fa33e86 100644 --- a/Downterm/CommandNotch/Views/SettingsView.swift +++ b/Downterm/CommandNotch/Views/SettingsView.swift @@ -265,6 +265,7 @@ struct TerminalSettingsView: View { @AppStorage(NotchSettings.Keys.terminalFontSize) private var fontSize = NotchSettings.Defaults.terminalFontSize @AppStorage(NotchSettings.Keys.terminalShell) private var shellPath = NotchSettings.Defaults.terminalShell + @AppStorage(NotchSettings.Keys.terminalTheme) private var theme = NotchSettings.Defaults.terminalTheme var body: some View { Form { @@ -275,6 +276,21 @@ struct TerminalSettingsView: View { Text("\(Int(fontSize))pt").monospacedDigit().frame(width: 50) } } + Section("Colors") { + Picker("Theme", selection: $theme) { + ForEach(TerminalTheme.allCases) { terminalTheme in + Text(terminalTheme.label).tag(terminalTheme.rawValue) + } + } + + Text(TerminalTheme.resolve(theme).detail) + .font(.caption) + .foregroundStyle(.secondary) + + Text("Applies to normal terminal text and the ANSI palette used by tools like `ls`.") + .font(.caption) + .foregroundStyle(.secondary) + } Section("Shell") { TextField("Shell path (empty = $SHELL)", text: $shellPath) .textFieldStyle(.roundedBorder)