Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
/* English is the default UI language. */
"Appearance" = "Appearance";
"Theme" = "Theme";
"Color theme" = "Color theme";
"Appearance mode" = "Appearance mode";
"Lithe" = "Lithe";
"Codex" = "Codex";
"Linear" = "Linear";
"System" = "System";
"Light" = "Light";
"Dark" = "Dark";
"Choose whether Lithe follows the system appearance or always uses a light or dark theme." = "Choose whether Lithe follows the system appearance or always uses a light or dark theme.";
"Choose a color theme and whether Lithe follows the system appearance." = "Choose a color theme and whether Lithe follows the system appearance.";
"Editor tabs" = "Editor tabs";
"Single row" = "Single row";
"Wrap into rows" = "Wrap into rows";
Expand Down
12 changes: 12 additions & 0 deletions Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
"Close Settings" = "关闭设置";
"Project" = "项目";
"General" = "通用";
"Appearance" = "外观";
"Theme" = "主题";
"Color theme" = "配色主题";
"Appearance mode" = "外观模式";
"Lithe" = "Lithe";
"Codex" = "Codex";
"Linear" = "Linear";
"System" = "跟随系统";
"Light" = "浅色";
"Dark" = "深色";
"Choose whether Lithe follows the system appearance or always uses a light or dark theme." = "选择跟随系统外观,或始终使用浅色或深色主题。";
"Choose a color theme and whether Lithe follows the system appearance." = "选择配色主题,并设置是否跟随系统外观。";
"Editor" = "编辑器";
"Terminal" = "终端";
"Updates" = "更新";
Expand Down
27 changes: 13 additions & 14 deletions Sources/Lithe/LitheApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,9 @@ import SwiftUI
@MainActor
final class LitheAppDelegate: NSObject, NSApplicationDelegate {
weak var projectSessions: ProjectSessionManager?
var mainWindow: NSWindow?

func registerMainWindow(_ window: NSWindow) {
mainWindow = window
}

func applicationShouldHandleReopen(
_ sender: NSApplication,
hasVisibleWindows: Bool
) -> Bool {
guard !hasVisibleWindows, let mainWindow else { return true }
mainWindow.makeKeyAndOrderFront(nil)
sender.activate(ignoringOtherApps: true)
return false
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
true
}

func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
Expand Down Expand Up @@ -97,7 +86,7 @@ struct LitheApp: App {
// changes. Re-identify the root so a language selection takes
// effect immediately across every workspace, including sheets.
.id(settings.language)
.preferredColorScheme(.dark)
.preferredColorScheme(settings.themePreference.preferredColorScheme)
.task {
memoryUsageMonitor.start()
}
Expand Down Expand Up @@ -245,3 +234,13 @@ struct LitheApp: App {
)
}
}

private extension AppThemePreference {
var preferredColorScheme: ColorScheme? {
switch self {
case .system: nil
case .light: .light
case .dark: .dark
}
}
}
73 changes: 73 additions & 0 deletions Sources/Lithe/Models/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Foundation
@MainActor
final class AppSettings: ObservableObject {
private enum Key {
static let colorTheme = "settings.colorTheme"
static let themePreference = "settings.themePreference"
static let language = "settings.language"
static let editorFontSize = "settings.editorFontSize"
static let tabWidth = "settings.tabWidth"
Expand All @@ -20,6 +22,15 @@ final class AppSettings: ObservableObject {

private let defaults: any KeyValueStore

@Published var colorTheme: AppColorTheme {
didSet {
AppThemeRuntime.shared.activate(colorTheme)
defaults.set(colorTheme.rawValue, forKey: Key.colorTheme)
}
}
@Published var themePreference: AppThemePreference {
didSet { defaults.set(themePreference.rawValue, forKey: Key.themePreference) }
}
@Published var language: AppLanguage { didSet { defaults.set(language.rawValue, forKey: Key.language) } }
@Published var editorFontSize: Double { didSet { defaults.set(editorFontSize, forKey: Key.editorFontSize) } }
@Published var tabWidth: Int { didSet { defaults.set(tabWidth, forKey: Key.tabWidth) } }
Expand Down Expand Up @@ -56,6 +67,12 @@ final class AppSettings: ObservableObject {

init(store: any KeyValueStore) {
self.defaults = store
colorTheme = AppColorTheme(
rawValue: defaults.string(forKey: Key.colorTheme) ?? ""
) ?? .lithe
themePreference = AppThemePreference(
rawValue: defaults.string(forKey: Key.themePreference) ?? ""
) ?? .dark
language = AppLanguage(rawValue: defaults.string(forKey: Key.language) ?? "") ?? .english
editorFontSize = defaults.object(forKey: Key.editorFontSize) as? Double ?? 13
tabWidth = defaults.object(forKey: Key.tabWidth) as? Int ?? 4
Expand All @@ -82,6 +99,7 @@ final class AppSettings: ObservableObject {
} else {
commitMessageAI = .default
}
AppThemeRuntime.shared.activate(colorTheme)
}

var terminalShellPath: String? { terminalShell.path }
Expand Down Expand Up @@ -111,6 +129,8 @@ final class AppSettings: ObservableObject {
}

func restoreDefaults() {
colorTheme = .lithe
themePreference = .dark
language = .english
editorFontSize = 13
tabWidth = 4
Expand Down Expand Up @@ -212,6 +232,59 @@ final class AppSettings: ObservableObject {
}
}

enum AppColorTheme: String, CaseIterable, Identifiable {
case lithe
case codex
case linear

var id: String { rawValue }

var title: String {
switch self {
case .lithe: "Lithe"
case .codex: "Codex"
case .linear: "Linear"
}
}
}

final class AppThemeRuntime: @unchecked Sendable {
static let shared = AppThemeRuntime()

private let lock = NSLock()
private var value: AppColorTheme = .lithe

private init() {}

func activate(_ theme: AppColorTheme) {
lock.lock()
value = theme
lock.unlock()
}

var activeTheme: AppColorTheme {
lock.lock()
defer { lock.unlock() }
return value
}
}

enum AppThemePreference: String, CaseIterable, Identifiable {
case system
case light
case dark

var id: String { rawValue }

var title: String {
switch self {
case .system: "System"
case .light: "Light"
case .dark: "Dark"
}
}
}

enum EditorTabLayoutMode: String, CaseIterable, Identifiable {
case singleLine
case multipleRows
Expand Down
47 changes: 23 additions & 24 deletions Sources/Lithe/Platform/MacOS/Terminal/MacTerminalTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ import SwiftTerm
final class LitheTerminalView: LocalProcessTerminalView {
var onOpenLink: ((String, [String: String]) -> Void)?

override func viewDidChangeEffectiveAppearance() {
super.viewDidChangeEffectiveAppearance()
applyThemeColors()
}

func applyThemeColors() {
let isDark = effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
nativeBackgroundColor = isDark
? NSColor(srgbRed: 0.071, green: 0.075, blue: 0.081, alpha: 1)
: NSColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
nativeForegroundColor = isDark
? NSColor(srgbRed: 0.86, green: 0.87, blue: 0.89, alpha: 1)
: NSColor(srgbRed: 0.15, green: 0.16, blue: 0.18, alpha: 1)
caretColor = isDark
? NSColor(srgbRed: 0.35, green: 0.67, blue: 0.98, alpha: 1)
: NSColor(srgbRed: 0.18, green: 0.43, blue: 0.79, alpha: 1)
selectedTextBackgroundColor = isDark
? NSColor(srgbRed: 0.16, green: 0.31, blue: 0.48, alpha: 1)
: NSColor(srgbRed: 0.69, green: 0.82, blue: 0.98, alpha: 1)
needsDisplay = true
}

override func requestOpenLink(source: SwiftTerm.TerminalView, link: String, params: [String: String]) {
onOpenLink?(link, params)
}
Expand Down Expand Up @@ -47,30 +69,7 @@ final class MacTerminalTransport: NSObject, TerminalTransport, @preconcurrency L
self?.onLink?(link, params)
}
view.font = Self.preferredTerminalFont()
view.nativeBackgroundColor = NSColor(
calibratedRed: 0.071,
green: 0.075,
blue: 0.081,
alpha: 1
)
view.nativeForegroundColor = NSColor(
calibratedRed: 0.86,
green: 0.87,
blue: 0.89,
alpha: 1
)
view.caretColor = NSColor(
calibratedRed: 0.35,
green: 0.67,
blue: 0.98,
alpha: 1
)
view.selectedTextBackgroundColor = NSColor(
calibratedRed: 0.16,
green: 0.31,
blue: 0.48,
alpha: 1
)
view.applyThemeColors()
view.allowMouseReporting = true
view.linkReporting = .implicit
view.linkHighlightMode = .hoverWithModifier
Expand Down
9 changes: 9 additions & 0 deletions Sources/Lithe/Theme/LitheIcons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ enum LitheIcons {
// MARK: - SwiftUI 图标

struct LitheIcon: View {
@Environment(\.colorScheme) private var colorScheme
let kind: LitheIconKind
var size: CGFloat = 14

Expand All @@ -464,6 +465,11 @@ struct LitheIcon: View {
Image(nsImage: image)
.resizable()
.interpolation(.high)
// IntelliJ's catalog is authored against a dark canvas. A
// small contrast normalization keeps its semantic colors
// legible on light surfaces without turning them monochrome.
.saturation(colorScheme == .light ? 0.94 : 1)
.contrast(colorScheme == .light ? 0.90 : 1)
.frame(width: size, height: size)
} else {
switch LitheIcons.appearance(for: kind) {
Expand All @@ -488,6 +494,7 @@ struct LitheIDEAIcon: View {
var body: some View {
if let image = LitheIcons.ideaImage(resourcePath: resourcePath) {
Image(nsImage: image)
.renderingMode(.template)
.resizable()
.interpolation(.high)
.frame(width: size, height: size)
Expand Down Expand Up @@ -517,6 +524,8 @@ struct LitheSystemIcon: View {
)
} else {
Image(systemName: systemImage)
.font(.system(size: size, weight: .medium))
.frame(width: size, height: size)
}
}
}
Expand Down
Loading
Loading